diff --git a/.dockerignore b/.dockerignore index 33f1d990..1eb63a0d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,40 +1,24 @@ -**/.* -**/.git -**/.gitignore -**/.github -**/.vscode -**/.idea -**/coverage -**/.aws -**/.ssh -**/.DS_Store -**/.aof -**/venv -**/.venv -**/env -**/bin -# **/docs we want to keep dje/templates/rest_framework/docs/ -docs/ -# **/dist we want to keep ./thirdparty/dist/ -# **/etc we need to keep ./etc/ -**/lib -**/include -**/share -**/var -**/*.egg-info -**/*.log -**/__pycache__ -**/.*cache -*.pyc -.dockerignore -.readthedocs.yaml -docker.env -.env -Makefile -Dockerfile -README.rst -CHANGELOG.rst -CONTRIBUTING.rst -MANIFEST.in -docker-compose.yml -pyvenv.cfg +# Ignore everything +* + +# Allow only what the build needs +!aboutcode/ +!component_catalog/ +!data/ +!dejacode/ +!dejacode_toolkit/ +!dje/ +!license_library/ +!notification/ +!organization/ +!policy/ +!product_portfolio/ +!purldb/ +!reporting/ +!thirdparty/ +!vulnerabilities/ +!workflow/ +!LICENSE +!manage.py +!NOTICE +!pyproject.toml diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..62ef36ff --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,14 @@ +## Issues + +- Closes: # + +## Changes + + + +## Checklist + +- [ ] I have read the [contributing guidelines](https://github.com/aboutcode-org/dejacode/blob/main/CONTRIBUTING.md) +- [ ] I have linked an existing issue above +- [ ] I have added unit tests covering the new code +- [ ] I have reviewed and understood every line of this PR diff --git a/.github/workflows/create-github-release.yml b/.github/workflows/create-github-release.yml index 6d9a3e09..cc65a3a7 100644 --- a/.github/workflows/create-github-release.yml +++ b/.github/workflows/create-github-release.yml @@ -9,10 +9,16 @@ on: jobs: create-github-release: runs-on: ubuntu-24.04 + permissions: + contents: write # needed to create releases and upload assets steps: - - name: Create a GitHub release - uses: softprops/action-gh-release@v2 + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: - generate_release_notes: true - draft: false + persist-credentials: false + + - name: Create a GitHub release + run: gh release create "$GITHUB_REF_NAME" --generate-notes + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/find-vulnerabilities.yml b/.github/workflows/find-vulnerabilities.yml index d227aa43..fef2330c 100644 --- a/.github/workflows/find-vulnerabilities.yml +++ b/.github/workflows/find-vulnerabilities.yml @@ -1,39 +1,33 @@ name: Find dependencies vulnerabilities -on: [push] +on: + workflow_dispatch: + pull_request: + push: + branches: + - main jobs: scan-codebase: runs-on: ubuntu-24.04 + permissions: + contents: read + name: Inspect packages with ScanCode.io steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: path: scancode-inputs - sparse-checkout: setup.cfg + sparse-checkout: pyproject.toml sparse-checkout-cone-mode: false + persist-credentials: false # do not keep the token around - - uses: nexB/scancode-action@alpha + - name: Fail on known vulnerabilities + uses: aboutcode-org/scancode-action@76777db8400d719de67ba3e465c5881037b45cb9 # v0.1 with: pipelines: "inspect_packages:StaticResolver,find_vulnerabilities" + check-compliance: true + compliance-fail-on-vulnerabilities: true + scancodeio-repo-branch: "main" env: VULNERABLECODE_URL: https://public.vulnerablecode.io/ - - - name: Fail in case of vulnerabilities - shell: bash - run: | - scanpipe shell --command ' - from scanpipe.models import Project - project = Project.objects.get() - packages_qs = project.discoveredpackages.vulnerable() - dependencies_qs = project.discovereddependencies.vulnerable() - vulnerability_count = packages_qs.count() + dependencies_qs.count() - if vulnerability_count: - print(vulnerability_count, "vulnerabilities found:") - for entry in [*packages_qs, *dependencies_qs]: - print(entry) - exit(1) - else: - print("No vulnerabilities found") - exit(0) - ' diff --git a/.github/workflows/publish-docker-image.yml b/.github/workflows/publish-docker-image.yml index 8e09a6ca..9bc57521 100644 --- a/.github/workflows/publish-docker-image.yml +++ b/.github/workflows/publish-docker-image.yml @@ -15,22 +15,26 @@ env: IMAGE_NAME: ${{ github.repository }} jobs: - build-and-push-image: + build-and-publish-image: runs-on: ubuntu-24.04 # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job. permissions: contents: read packages: write + attestations: write + id-token: write steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false # do not keep the token around # Uses the `docker/login-action` action to log in to the Container registry using # the account and password that will publish the packages. - name: Log in to the Container registry - uses: docker/login-action@v3 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -42,7 +46,7 @@ jobs: # The `images` value provides the base name for the tags and labels. - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} @@ -53,11 +57,22 @@ jobs: # It uses the `tags` and `labels` parameters to tag and label the image with # the output from the "meta" step. - name: Build and push Docker image - uses: docker/build-push-action@v5 + id: push + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 with: context: . push: true tags: | ${{ steps.meta.outputs.tags }} - ${{ env.REGISTRY }}/aboutcode-org/dejacode:latest + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest labels: ${{ steps.meta.outputs.labels }} + + # This step generates an artifact attestation for the image, which is an + # unforgeable statement about where and how it was built. + # It increases supply chain security for people who consume the image. + - name: Generate artifact attestation + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 + with: + subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}} + subject-digest: ${{ steps.push.outputs.digest }} + push-to-registry: true diff --git a/.github/workflows/publish-pypi-release-aboutcode-api-auth.yml b/.github/workflows/publish-pypi-release-aboutcode-api-auth.yml new file mode 100644 index 00000000..61a6fabd --- /dev/null +++ b/.github/workflows/publish-pypi-release-aboutcode-api-auth.yml @@ -0,0 +1,67 @@ +name: Build aboutcode.api_auth Python distributions and publish on PyPI + +on: + workflow_dispatch: + push: + tags: + - "aboutcode.api_auth/*" + +env: + PYPI_PROJECT_URL: "https://pypi.org/p/aboutcode.api_auth" + PYPROJECT_TOML: "api_auth-pyproject.toml" + FLOT_VERSION: "0.7.2" + +jobs: + build: + name: Build and publish library to PyPI + runs-on: ubuntu-24.04 + permissions: + contents: read + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false # do not keep the token around + + - name: Set up Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: 3.14 + + - name: Install flot + run: python -m pip install "flot==${FLOT_VERSION}" --user + + - name: Build a binary wheel and a source tarball + run: python -m flot --pyproject "$PYPROJECT_TOML" --sdist --wheel --output-dir dist/ + + - name: Upload package distributions as GitHub workflow artifacts + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: python-package-distributions + path: dist/ + + # Only set the id-token: write permission in the job that does publishing, not globally. + # Also, separate building from publishing, this makes sure that any scripts + # maliciously injected into the build or test environment won't be able to elevate + # privileges while flying under the radar. + pypi-publish: + name: Upload package distributions to PyPI + if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes + needs: + - build + runs-on: ubuntu-24.04 + environment: + name: pypi + url: ${{ env.PYPI_PROJECT_URL }} + permissions: + id-token: write # IMPORTANT: this permission is mandatory for trusted publishing + + steps: + - name: Download all the dists + uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0 + with: + name: python-package-distributions + path: dist/ + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 diff --git a/.github/workflows/run-unit-tests-docker.yml b/.github/workflows/run-unit-tests-docker.yml index 1aefb5ed..53de574b 100644 --- a/.github/workflows/run-unit-tests-docker.yml +++ b/.github/workflows/run-unit-tests-docker.yml @@ -1,18 +1,23 @@ name: Run unit tests on Docker container on: - push: - branches: [ main ] + workflow_dispatch: pull_request: - branches: [ main ] + push: + branches: + - main jobs: - test: + run-unit-tests: runs-on: ubuntu-24.04 + permissions: + contents: read steps: - - name: Checkout code - uses: actions/checkout@v4 + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false # do not keep the token around - name: Generate the .env file and the SECRET_KEY run: make envfile diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index a0c111eb..06ecbf4b 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -1,10 +1,11 @@ name: Run unit tests on: - push: - branches: [ main ] + workflow_dispatch: pull_request: - branches: [ main ] + push: + branches: + - main env: DATABASE_NAME: dejacode @@ -13,12 +14,14 @@ env: POSTGRES_INITDB_ARGS: --encoding=UTF-8 --lc-collate=en_US.UTF-8 --lc-ctype=en_US.UTF-8 jobs: - test: + run-unit-tests: runs-on: ubuntu-24.04 + permissions: + contents: read services: postgres: - image: postgres:16 + image: postgres:16.13 env: POSTGRES_DB: ${{ env.DATABASE_NAME }} POSTGRES_USER: ${{ env.DATABASE_USER }} @@ -34,17 +37,24 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false # do not keep the token around - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: - python-version: "3.12" + python-version: "3.14" - name: Install python-ldap OS dependencies run: | sudo apt-get update - sudo apt-get install -y libldap2-dev libsasl2-dev + sudo apt-get install -y libldap2-dev libsasl2-dev slapd ldap-utils + + - name: Disable AppArmor for slapd + run: | + sudo ln -s /etc/apparmor.d/usr.sbin.slapd /etc/apparmor.d/disable/ + sudo apparmor_parser -R /etc/apparmor.d/usr.sbin.slapd - name: Install dependencies run: make dev envfile @@ -53,7 +63,7 @@ jobs: run: make check - name: Start Redis - uses: supercharge/redis-github-action@1.5.0 + uses: supercharge/redis-github-action@bc274cb7238cd63a45029db04ee48c07a72609fd # v1.8.1 - name: Build the documentation run: make docs diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 19c47475..bd1eb5ae 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -6,14 +6,12 @@ version: 2 # Set the OS, Python version and other tools you might need build: - os: ubuntu-22.04 + os: ubuntu-24.04 tools: - python: "3.12" - -# Optionally declare the Python requirements required to build your docs -python: - install: - - requirements: docs/requirements.txt + python: "3.13" + jobs: + post_install: + - pip install --upgrade furo # Build documentation in the "docs/" directory with Sphinx sphinx: diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c1997969..519915f6 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,7 +1,207 @@ Release notes ============= -### Version 5.3.0-dev +### Version 5.7.1 + +- feat: Product compliance tab + https://github.com/aboutcode-org/dejacode/issues/507 + +- feat: Compliance control center dashboard + https://github.com/aboutcode-org/dejacode/issues/513 + +- feat: refine package search to display the most recent versions first + https://github.com/aboutcode-org/dejacode/issues/504 + +- fix: increase the max_length to 255 for api key fields + https://github.com/aboutcode-org/dejacode/issues/518 + +- fix: product is_vulnerable filter on list view + https://github.com/aboutcode-org/dejacode/issues/516 + +### Version 5.7.0 + +- Upgrade Python version to 3.14 + https://github.com/aboutcode-org/dejacode/pull/465 + +- Update Django to version 6.x + https://github.com/aboutcode-org/dejacode/pull/466 + +- Fix parsing of str into timezone aware dates in reporting + https://github.com/aboutcode-org/dejacode/pull/461 + +- Set usage policy from license profile + https://github.com/aboutcode-org/dejacode/pull/463 + +- Add support for OpenDocument format in report export + https://github.com/aboutcode-org/dejacode/pull/478 + +- Form validation on permission protected fields + https://github.com/aboutcode-org/dejacode/pull/479 + +- Fix stream scan results data instead of silencing timeouts + https://github.com/aboutcode-org/dejacode/pull/481 + +- Fix upgrade RQ to fix a worker failure + https://github.com/aboutcode-org/dejacode/pull/483 + +- Replace plain-text DRF token with PBKDF2-hashed API token + https://github.com/aboutcode-org/dejacode/pull/484 + +- Fix rendering of the burger menu as offcanvas + https://github.com/aboutcode-org/dejacode/pull/486 + +- Add ability to revoke an API key from profile view + https://github.com/aboutcode-org/dejacode/pull/491 + +- Rework the pagination with per-model setting + https://github.com/aboutcode-org/dejacode/pull/494 + +- Add generic views for API key management in `aboutcode.api_auth` module + https://github.com/aboutcode-org/dejacode/pull/500 + +### Version 5.6.0 + +- feat: import vulnerability data from ScanCode.io + https://github.com/aboutcode-org/dejacode/issues/448 + +- feat: ability to assign and manage vulnerabilities on products + https://github.com/aboutcode-org/dejacode/issues/439 + +- feat: add package_content PurlDB field on Package model + https://github.com/aboutcode-org/dejacode/issues/434 + +- Fix exclude qualifiers and subpath for PURL comparison in get_purldb_entries + https://github.com/aboutcode-org/dejacode/issues/453 + +- Fix update the readthedocs.yml config to fix the build + https://github.com/aboutcode-org/dejacode/issues/447 + +- chore: upgrade Django and related libraries to latest version + https://github.com/aboutcode-org/dejacode/issues/451 + +- chore: upgrade altcha and django_altcha to latest versions + https://github.com/aboutcode-org/dejacode/issues/450 + +### Version 5.5.0 + +- Add UI to refresh a package scan. + https://github.com/aboutcode-org/dejacode/issues/423 + +- Update ProductPackage "unknown" license during "Scan all Packages". + Only "unknown" licenses are updated. + Products with a is_locked configuration status are excluded. + Inactive is_active=False products are excluded. + https://github.com/aboutcode-org/dejacode/issues/388 + +- Allow Product "Scan all packages" for users with the "change_product" permission + on the Product instance. + Prior to this change only "superusers" could see and use this feature. + https://github.com/aboutcode-org/dejacode/issues/385 + +- Add Dataspace FK validation on Dataspace and DejacodeUser models. + Assigning an object from another Dataspace will raise an error at the ``save()`` + level. + Do not include the ``homepage_layout`` field on Dataspace "addition" form since the + Dataspace does not exist yet. + Display the ``homepage_layout`` field as read-only on the Dataspace and User change + forms when the currently logged user is not looking at his own Dataspace. + https://github.com/aboutcode-org/dejacode/issues/428 + +- Prioritize hashes and download URL for PurlDB mapping. + https://github.com/aboutcode-org/dejacode/issues/430 + +- Fix a bug with the scan_status_fields on empty runs. + https://github.com/aboutcode-org/dejacode/issues/433 + +- Add option to infer_download_urls on product importers. + https://github.com/aboutcode-org/dejacode/issues/444 + +- Add support for PyPI purls in purl resolution. + https://github.com/aboutcode-org/dejacode/pull/443 + +- Export OpenVEX VEX document. + https://github.com/aboutcode-org/dejacode/issues/442 + +- Migrate from rq-scheduler to new built-in CronScheduler. + https://github.com/aboutcode-org/dejacode/issues/435 + +- Update weighted_risk_score on updating the relationship. + https://github.com/aboutcode-org/dejacode/issues/436 + +### Version 5.4.2 + +- Migrate the LDAP testing from using mockldap to slapdtest. + The mockldap and funcparserlib dependencies has been removed. + https://github.com/aboutcode-org/dejacode/issues/394 + +### Version 5.4.1 + +- Upgrade Django to latest security release 5.2.7 + Also upgrade dependencies to latest releases. + https://github.com/aboutcode-org/dejacode/pull/389 + +### Version 5.4.0 + +- Upgrade Python version to 3.13 and Django to 5.2.x + https://github.com/aboutcode-org/dejacode/pull/315 + https://github.com/aboutcode-org/dejacode/pull/312 + +- Replace the setup.py/setup.cfg by pyproject.toml file. + https://github.com/aboutcode-org/dejacode/pull/329 + +- Replace the hardcoded ``/var/www/html`` by a ``webroot`` named volume in + ``docker-compose.yml``. + In the Docker compose ``nginx`` service, the hardcoded ``/var/www/html`` was declared + as a volume which would cause issues when the ``/var/`` was not available on the + local machine. + The Docker volume directory ``/var/lib/docker/volumes/dejacode_webroot/_data`` can + now be used in place of ``/var/www/html`` as the ``certbot --webroot-path`` for + HTTP certificates management. + https://github.com/aboutcode-org/dejacode/issues/157 + https://github.com/aboutcode-org/dejacode/pull/322 + +- Add REST API "actions" in package endpoint to track the scan status and fetch results: + * `/packages/{uuid}/scan_info/` Scan information including the current status. + * `/packages/{uuid}/scan_results/` Scan results. + * `/packages/{uuid}/scan_summary/` Scan summary. + * `/packages/{uuid}/scan_data_download_zip/` Download all scan data: results and + summary, as a zip file. + https://github.com/aboutcode-org/dejacode/issues/272 + +- Add new `is_locked` "Locked inventory" field to the ProductStatus model. + When a Product is locked through his status, its inventory cannot be modified. + https://github.com/aboutcode-org/dejacode/issues/189 + +- Add support for the following fields in ScanCode.io scan results to package: + download_url, repository_download_url, repository_homepage_url, bug_tracking_url, + code_view_url, vcs_url, api_data_url, size, md5, sha1, sha256, sha512. + https://github.com/aboutcode-org/dejacode/issues/255 + +- Add a Product REST API "action" endpoint to track product imports and their status: + * `/products/{uuid}/imports/` + https://github.com/aboutcode-org/dejacode/issues/273 + +- Add GitHub workflow Request integration. + Documentation: https://dejacode.readthedocs.io/en/latest/integrations-github.html + https://github.com/aboutcode-org/dejacode/issues/349 + +- Add GitLab workflow Request integration. + Documentation: https://dejacode.readthedocs.io/en/latest/integrations-gitlab.html + https://github.com/aboutcode-org/dejacode/issues/346 + +- Add Jira workflow Request integration. + Documentation: https://dejacode.readthedocs.io/en/latest/integrations-jira.html + https://github.com/aboutcode-org/dejacode/issues/350 + +- Add Forgejo workflow Request integration. + Documentation: https://dejacode.readthedocs.io/en/latest/integrations-forgejo.html + https://github.com/aboutcode-org/dejacode/issues/347 + +- Add SourceHut workflow Request integration. + Documentation: https://dejacode.readthedocs.io/en/latest/integrations-sourcehut.html + https://github.com/aboutcode-org/dejacode/issues/348 + +### Version 5.3.0 - Rename ProductDependency is_resolved to is_pinned. https://github.com/aboutcode-org/dejacode/issues/189 @@ -123,6 +323,40 @@ Release notes - Refine the way the PURL fragments are handled in searches. https://github.com/aboutcode-org/dejacode/issues/286 +- Fix an issue with ``urlize_target_blank`` when the URL contains curly braces. + +- Add the ability to download Product "Imports" input file. + https://github.com/aboutcode-org/dejacode/issues/156 + +- Fix a logic issue in the ``ImportPackageFromScanCodeIO.import_package`` that occurs when + multiple packages with the same PURL, but different download_url or filename, + are present in the Dataspace. + https://github.com/aboutcode-org/dejacode/issues/295 + +- Fix a logic issue in the ``ImportPackageFromScanCodeIO.import_dependencies`` to + prevent the creation of duplicated "resolved" dependencies. + https://github.com/aboutcode-org/dejacode/issues/297 + +- Display the filename/download_url in the Inventory tab. + https://github.com/aboutcode-org/dejacode/issues/303 + +- Improve exception support in improve_packages_from_purldb task. + In case of an exception, the error is properly logged on the Import instance. + https://github.com/aboutcode-org/dejacode/issues/303 + +- Refine the ``update_from_purldb`` function to avoid any IntegrityError. + Also, when multiple entries are returned from the PurlDB, only the common values are + merged and kept for the data update. + https://github.com/aboutcode-org/dejacode/issues/303 + +- Add a new "Package Set" tab to the Package details view. + This tab displays related packages grouped by their normalized ("plain") Package URL. + https://github.com/aboutcode-org/dejacode/issues/276 + +- Refine get_purldb_entries to compare on plain PackageURL. + Including the qualifiers and subpaths in the comparison was too restrictive. + https://github.com/aboutcode-org/dejacode/issues/307 + ### Version 5.2.1 - Fix the models documentation navigation. @@ -131,6 +365,9 @@ Release notes - Fix the validity of SPDX outputs. https://github.com/aboutcode-org/dejacode/issues/180 +- Add ability to start and delete package scans from the Product inventory tab. + https://github.com/aboutcode-org/dejacode/pull/281 + ### Version 5.2.0 - Add visual indicator in hierarchy views, when an object on the far left or far right diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..a9218c99 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,45 @@ +# Contributing to DejaCode + +Thank you for your interest in contributing to AboutCode projects. +Please **read the following guidelines carefully before submitting a pull request**. + +## Pull request rules + +- **Attach your PR to an existing issue.** Discuss the approach and implementation + details in the issue first, before writing any code. Please note that if the issue + has already been assigned to someone else, you are welcome to comment on it, but it + would be best to contribute code in such a case only if you are invited to do so. + +- **You must fully understand your code changes** and be able to explain them + clearly. If you cannot walk a reviewer through your changes, the PR will not + be accepted. + +- **Write your own descriptions, comments, and documentation.** All written + content in a PR must be authored by a human. + +- **Disclose any AI usage.** If any part of your contribution was generated or + assisted by AI, you must disclose this and specify the tools used. + +- **Include tests.** Any code change or addition must be accompanied by proper + unit tests. Untested code will not be merged. + +**Any PR that violates these rules will be closed.** + +## Pull request tips + +To maximize your chances of a successful contribution: + +- **Keep changes focused.** Address only what the issue requires. Do not bundle + unrelated changes together. + +- **Be patient.** Do not solicit maintainers for a review. + +- **Remember:** project maintainers have no interest in reviewing code + generated by machines. + +- **Do not make structural changes** to the codebase such as adding type hints. + +## Ways to Contribute + +See https://dejacode.readthedocs.io/en/latest/contributing.html for more ways to +contribute. diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst deleted file mode 100644 index 9f8bf81d..00000000 --- a/CONTRIBUTING.rst +++ /dev/null @@ -1,6 +0,0 @@ -======================== -Contributing to DejaCode -======================== - -Documented at https://dejacode.readthedocs.io/en/latest/contributing.html -or in docs/contributing.rst for details. diff --git a/Dockerfile b/Dockerfile index 3c627e99..058abaea 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ # See https://aboutcode.org for more information about AboutCode FOSS projects. # -FROM python:3.12-slim +FROM python:3.14-slim LABEL org.opencontainers.image.source="https://github.com/aboutcode-org/dejacode" LABEL org.opencontainers.image.description="DejaCode" @@ -27,8 +27,8 @@ ENV VENV_LOCATION=/opt/$APP_NAME/.venv ENV PYTHONUNBUFFERED=1 # Do not write Python .pyc files ENV PYTHONDONTWRITEBYTECODE=1 -# Add the app dir in the Python path for entry points availability -ENV PYTHONPATH=$PYTHONPATH:$APP_DIR +# Set the app dir in the Python path for entry points availability +ENV PYTHONPATH=$APP_DIR # OS requirements RUN apt-get update \ @@ -36,6 +36,8 @@ RUN apt-get update \ build-essential \ libldap2-dev \ libsasl2-dev \ + slapd \ + ldap-utils \ libpq5 \ git \ wait-for-it \ @@ -62,7 +64,7 @@ RUN python -m venv $VENV_LOCATION ENV PATH=$VENV_LOCATION/bin:$PATH # Install the dependencies before the codebase COPY for proper Docker layer caching -COPY --chown=$APP_USER:$APP_USER setup.cfg setup.py $APP_DIR/ +COPY --chown=$APP_USER:$APP_USER pyproject.toml $APP_DIR/ COPY --chown=$APP_USER:$APP_USER ./thirdparty/dist/ $APP_DIR/thirdparty/dist/ RUN pip install --find-links=$APP_DIR/thirdparty/dist/ --no-index --no-cache-dir . diff --git a/Makefile b/Makefile index 6730881e..3a976ae2 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ # See https://aboutcode.org for more information about AboutCode FOSS projects. # -PYTHON_EXE=python3.12 +PYTHON_EXE=python3.14 VENV_LOCATION=.venv ACTIVATE?=. ${VENV_LOCATION}/bin/activate; MANAGE=${VENV_LOCATION}/bin/python manage.py @@ -25,6 +25,7 @@ DB_CONTAINER_NAME=db DB_INIT_FILE=./data/postgresql/initdb.sql.gz POSTGRES_INITDB_ARGS=--encoding=UTF-8 --lc-collate=en_US.UTF-8 --lc-ctype=en_US.UTF-8 TIMESTAMP=$(shell date +"%Y-%m-%d_%H%M") +IMAGE_NAME=dejacode # Use sudo for postgres, only on Linux UNAME := $(shell uname) @@ -58,11 +59,14 @@ envfile_dev: envfile @echo "-> Update the .env file for development" @echo DATABASE_PASSWORD=\"dejacode\" >> ${ENV_FILE} +doc_dependencies: virtualenv + @echo "-> Configure and install documentation dependencies" + @${ACTIVATE} pip install --editable .[docs] + doc8: - @echo "-> Run doc8 validation" - @${ACTIVATE} doc8 --max-line-length 100 --ignore-path docs/_build/ \ - --ignore-path docs/installation_and_sysadmin/ \ - --quiet docs/ + @echo "-> Run documentation .rst validation" + @$(MAKE) doc_dependencies > /dev/null 2>&1 + @${ACTIVATE} doc8 --max-line-length 100 --ignore-path docs/_build/ --quiet docs/ valid: @echo "-> Run Ruff format" @@ -136,7 +140,7 @@ postgresdb_clean: @${SUDO_POSTGRES} dropuser '${DB_USERNAME}' || true run: - ${MANAGE} runserver 8000 --insecure + DJANGO_RUNSERVER_HIDE_WARNING=true ${MANAGE} runserver 8000 --insecure worker: ${MANAGE} rqworker @@ -146,18 +150,18 @@ test: ${MANAGE} test --noinput --parallel auto docs: - @echo "-> Builds the installation_and_sysadmin docs" + @echo "-> Builds the documentation" rm -rf ${DOCS_LOCATION}/_build/ - @${ACTIVATE} pip install -r docs/requirements.txt + @$(MAKE) doc_dependencies > /dev/null 2>&1 @${ACTIVATE} sphinx-build -b singlehtml ${DOCS_LOCATION} ${DOCS_LOCATION}/_build/singlehtml/ @${ACTIVATE} sphinx-build -b html ${DOCS_LOCATION} ${DOCS_LOCATION}/_build/html/ build: - @echo "-> Build the Docker images" - ${DOCKER_COMPOSE} build + @echo "-> Build the Docker image" + docker build -t $(IMAGE_NAME) . bash: - ${DOCKER_EXEC} web bash + docker run -it $(IMAGE_NAME) bash shell: ${DOCKER_EXEC} web ./manage.py shell @@ -172,4 +176,4 @@ log: createsuperuser: ${DOCKER_EXEC} web ./manage.py createsuperuser -.PHONY: virtualenv conf dev envfile envfile_dev check doc8 valid check-deploy clean initdb postgresdb postgresdb_clean migrate upgrade run test docs build psql bash shell log createsuperuser +.PHONY: virtualenv conf dev envfile envfile_dev doc_dependencies check doc8 valid check-deploy clean initdb postgresdb postgresdb_clean migrate upgrade run test docs build psql bash shell log createsuperuser diff --git a/README.rst b/README.rst index b31f44ee..bee274be 100644 --- a/README.rst +++ b/README.rst @@ -1,42 +1,42 @@ +======== DejaCode ======== -DejaCode is a complete enterprise-level application to automate open source license -compliance and ensure software supply chain integrity, powered by -`ScanCode `_, +DejaCode provides an enterprise-level application to automate open source license +compliance and ensure software supply chain integrity, powered by `ScanCode `_, the industry-leading code scanner. -- Run scans and track all the open source and third-party products and components used - in your software. -- Apply usage policies at the license or component level, and integrate into - ScanCode to ensure compliance. -- Capture software inventories (SBOMs), generate compliance artifacts, and keep - historical data. -- Ensure FOSS compliance with enterprise-grade features and integrations for DevOps and - software systems. -- Scan a software package, simply by providing its Download URL, to get comprehensive - details of its composition and create an SBOM. -- Load software package data into DejaCode with the integration for the open source - ScanCode.io and ScanCode Toolkit projects to create a product’s SBOM. -- Track and report vulnerability tracking and reporting by integrating with the open - source VulnerableCode project. -- Create, publish and share SBOM documents in DejaCode, including detailed attribution - documentation and custom reports in multiple file formats and standards, such as - CycloneDX and SPDX. - -Getting started ---------------- - -The DejaCode documentation is available here: https://dejacode.readthedocs.io/ - -If you have questions please ask them in -`Discussions `_. - -If you want to contribute to DejaCode, start with our -`Contributing `_ page. +Why Use DejaCode? +================= + +DejaCode is your system of record as a single source of truth with quality data for +licenses, vulnerabilities, and package provenance and metadata, enabling you to ensure +FOSS compliance with enterprise-grade features and integrations for DevOps and +software systems. + +Getting Started +=============== + +Instructions to get you up and running on your local machine are at `Getting Started `_ + +The DejaCode documentation also provides: + +- prerequisites for installing the software. +- instructions for configuring DejaCode integration with `ScanCode.io `_, `VulnerableCode `_, and `PurlDB `_. +- tutorials that provide hands-on guidance to DejaCode features. +- how to setup usage policies. +- how to capture and share software inventories (SBOMs) in multiple file formats and standards, such as CycloneDX and SPDX. +- how to customize your own workflows and reports. +- guidelines for contributing to code development. + +Contributing +============ + +Thank you for your interest in contributing to AboutCode projects. +Please `read the following guidelines carefully `_ before getting started. Build and tests status ----------------------- +====================== +------------+-------------------+ | **Tests** | **Documentation** | @@ -45,7 +45,7 @@ Build and tests status +------------+-------------------+ DejaCode License Notice ------------------------ +======================= DejaCode is an enterprise-level application to automate open source license compliance and ensure software supply chain integrity, powered by ScanCode, @@ -68,15 +68,15 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . Commercial Services option ---------------------------- +========================== nexB offers a commercial services option for DejaCode. You can learn more about these options by contacting nexB at https://www.nexb.com/contact-us/ -.. |ci-tests| image:: https://github.com/aboutcode-org/dejacode/actions/workflows/ci.yml/badge.svg?branch=main - :target: https://github.com/aboutcode-org/dejacode/actions/workflows/ci.yml +.. |ci-tests| image:: https://github.com/aboutcode-org/dejacode/actions/workflows/run-unit-tests.yml/badge.svg?branch=main + :target: https://github.com/aboutcode-org/dejacode/actions/workflows/run-unit-tests.yml :alt: CI Tests Status .. |docs-rtd| image:: https://readthedocs.org/projects/dejacode/badge/?version=latest @@ -84,7 +84,7 @@ https://www.nexb.com/contact-us/ :alt: Documentation Build Status Acknowledgements, Funding, Support and Sponsoring --------------------------------------------------------- +================================================= This project is funded, supported and sponsored by: @@ -99,7 +99,6 @@ This project is funded, supported and sponsored by: - nexB Inc. - |europa| |dgconnect| |ngi| |nlnet| @@ -179,10 +178,3 @@ Communications Networks, Content and Technology under grant agreement No 1010695 :target: https://nlnet.nl/discovery/ :height: 40 :alt: NGI Discovery logo - - - - - - - diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 00000000..140b1850 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,20 @@ +# Release instructions for `DejaCode + +### Automated release workflow + +- Create a new `release-x.x.x` branch +- Update the version in: + - `pyproject.toml` + - `dejacode/__init__.py` + - `CHANGELOG.rst` (set date) +- Commit and push this branch +- Create a PR and merge once approved +- Tag and push that tag. This will trigger the `create-github-release.yml` + and `publish-docker-image.yml` GitHub workflows: + ``` + VERSION=vx.x.x # <- Set the new version here + git tag -a $VERSION -m "" + git push origin $VERSION + ``` +- Review the GitHub release created by the workflow at + https://github.com/aboutcode-org/dejacode/releases/ diff --git a/aboutcode/api_auth/README.md b/aboutcode/api_auth/README.md new file mode 100644 index 00000000..dd229108 --- /dev/null +++ b/aboutcode/api_auth/README.md @@ -0,0 +1,88 @@ +# `aboutcode.api_auth` + +Secured `APIToken` model and related `APITokenAuthentication` class. + +### Install + +```bash +pip install aboutcode.api_auth +``` + +### Define the APIToken model + +In your main `models.py` module: + +```python +from aboutcode.api_auth import AbstractAPIToken + +class APIToken(AbstractAPIToken): + class Meta: + verbose_name = "API Token" +``` + +Generate and apply schema migration: + +```bash +$ ./manage.py makemigrations +$ ./manage.py migrate +``` + +### Authenticator settings + +Declare your `APIToken` model location in the `API_TOKEN_MODEL` setting: + +```python +API_TOKEN_MODEL = "your_app.APIToken" # noqa: S105 +``` + +Declare the `APITokenAuthentication` authentication class as one of the +`REST_FRAMEWORK.DEFAULT_AUTHENTICATION_CLASSES`: + +```python +REST_FRAMEWORK = { + "DEFAULT_AUTHENTICATION_CLASSES": ( + "aboutcode.api_auth.APITokenAuthentication", + ), +} +``` + +### Views (optional) + +Base views are provided for generating and revoking API keys. +They handle the token operations and redirect with a success message. + +Subclass them in your app to add authentication requirements and configure +the success URL and message: + +```python +from django.contrib.auth.mixins import LoginRequiredMixin +from django.urls import reverse_lazy + +from aboutcode.api_auth.views import BaseGenerateAPIKeyView +from aboutcode.api_auth.views import BaseRevokeAPIKeyView + + +class GenerateAPIKeyView(LoginRequiredMixin, BaseGenerateAPIKeyView): + success_url = reverse_lazy("profile") + success_message = ( + "Copy your API key now, it will not be shown again:
{plain_key}
" + ) + + +class RevokeAPIKeyView(LoginRequiredMixin, BaseRevokeAPIKeyView): + success_url = reverse_lazy("profile") + success_message = "API key revoked." +``` + +Wire them up in your `urls.py`: + +```python +from your_app.views import GenerateAPIKeyView +from your_app.views import RevokeAPIKeyView + +urlpatterns = [ + ... + path("profile/api_key/generate/", GenerateAPIKeyView.as_view(), name="generate-api-key"), + path("profile/api_key/revoke/", RevokeAPIKeyView.as_view(), name="revoke-api-key"), +] +``` diff --git a/aboutcode/api_auth/RELEASE.md b/aboutcode/api_auth/RELEASE.md new file mode 100644 index 00000000..7ee4dfa8 --- /dev/null +++ b/aboutcode/api_auth/RELEASE.md @@ -0,0 +1,29 @@ +# Release instructions for `aboutcode.api_auth` + +### Automated release workflow + +- Create a new `aboutcode.api_auth-release-x.x.x` branch +- Update the version in: + - `api_auth-pyproject.toml` + - `aboutcode/api_auth/__init__.py` +- Commit and push this branch +- Create a PR and merge once approved +- Tag and push to trigger the `publish-pypi-release-aboutcode-api-auth.yml` workflow + that takes care of building the distribution archives and upload those to pypi:: + ``` + VERSION=x.x.x # <- Set the new version here + TAG=aboutcode.api_auth/$VERSION + git tag -a $TAG -m "" + git push origin $TAG + ``` + +### Manual build + +``` +cd dejacode +source .venv/bin/activate +pip install flot +flot --pyproject api_auth-pyproject.toml --sdist --wheel --output-dir dist/ +``` + +The distribution archives will be available in the local `dist/` directory. diff --git a/setup.py b/aboutcode/api_auth/__init__.py old mode 100755 new mode 100644 similarity index 52% rename from setup.py rename to aboutcode/api_auth/__init__.py index 914880b8..0680c333 --- a/setup.py +++ b/aboutcode/api_auth/__init__.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python - # # Copyright (c) nexB Inc. and others. All rights reserved. # DejaCode is a trademark of nexB Inc. @@ -8,7 +6,10 @@ # See https://aboutcode.org for more information about AboutCode FOSS projects. # -import setuptools +from aboutcode.api_auth.auth import APITokenAuthentication +from aboutcode.api_auth.models import AbstractAPIToken +from aboutcode.api_auth.models import get_api_token_model + +__version__ = "0.2.0" -if __name__ == "__main__": - setuptools.setup() +__all__ = ["APITokenAuthentication", "AbstractAPIToken", "get_api_token_model"] diff --git a/aboutcode/api_auth/auth.py b/aboutcode/api_auth/auth.py new file mode 100644 index 00000000..9cf932eb --- /dev/null +++ b/aboutcode/api_auth/auth.py @@ -0,0 +1,42 @@ +# +# Copyright (c) nexB Inc. and others. All rights reserved. +# DejaCode is a trademark of nexB Inc. +# SPDX-License-Identifier: AGPL-3.0-only +# See https://github.com/aboutcode-org/dejacode for support or download. +# See https://aboutcode.org for more information about AboutCode FOSS projects. +# + +from django.utils.translation import gettext_lazy as _ + +from rest_framework.authentication import TokenAuthentication +from rest_framework.exceptions import AuthenticationFailed + +from aboutcode.api_auth.models import get_api_token_model + + +class APITokenAuthentication(TokenAuthentication): + """ + Token authentication using a hashed API token for secure verification. + + Extends Django REST Framework's TokenAuthentication, replacing the plain-text lookup + with a prefix-based lookup and PBKDF2 hash verification. + """ + + model = None + + def get_model(self): + if self.model is not None: + return self.model + return get_api_token_model() + + def authenticate_credentials(self, plain_key): + model = self.get_model() + token = model.verify(plain_key) + + if token is None: + raise AuthenticationFailed(_("Invalid token.")) + + if not token.user.is_active: + raise AuthenticationFailed(_("User inactive or deleted.")) + + return (token.user, token) diff --git a/aboutcode/api_auth/models.py b/aboutcode/api_auth/models.py new file mode 100644 index 00000000..2160437e --- /dev/null +++ b/aboutcode/api_auth/models.py @@ -0,0 +1,103 @@ +# +# Copyright (c) nexB Inc. and others. All rights reserved. +# DejaCode is a trademark of nexB Inc. +# SPDX-License-Identifier: AGPL-3.0-only +# See https://github.com/aboutcode-org/dejacode for support or download. +# See https://aboutcode.org for more information about AboutCode FOSS projects. +# + +import secrets + +from django.apps import apps as django_apps +from django.conf import settings +from django.contrib.auth.hashers import check_password +from django.contrib.auth.hashers import make_password +from django.core.exceptions import ImproperlyConfigured +from django.db import models + + +class AbstractAPIToken(models.Model): + """ + API token using a lookup prefix and PBKDF2 hash for secure verification. + + The full key is never stored. Only a short plain-text prefix is kept for + DB lookup, and a hashed version of the full key is stored for verification. + The plain key is returned once at generation time and must be stored safely + by the client. + """ + + PREFIX_LENGTH = 8 + + key_hash = models.CharField( + max_length=128, + ) + user = models.OneToOneField( + settings.AUTH_USER_MODEL, + related_name="api_token", + on_delete=models.CASCADE, + ) + prefix = models.CharField( + max_length=PREFIX_LENGTH, + unique=True, + db_index=True, + ) + created = models.DateTimeField( + auto_now_add=True, + db_index=True, + ) + + class Meta: + abstract = True + + def __str__(self): + return f"APIToken {self.prefix}... ({self.user})" + + @classmethod + def generate_key(cls): + """Generate a plain (not encrypted) key.""" + return secrets.token_hex(32) + + def set_key(self, plain_key): + """Set the prefix and hashed key from a plain key. Does not save.""" + self.prefix = plain_key[: self.PREFIX_LENGTH] + self.key_hash = make_password(plain_key) + + @classmethod + def create_token(cls, user): + """Generate a new token for the given user and return the plain key once.""" + plain_key = cls.generate_key() + token = cls(user=user) + token.set_key(plain_key) + token.save() + return plain_key + + @classmethod + def verify(cls, plain_key): + """Return the token instance if the plain key is valid, None otherwise.""" + if not plain_key: + return + + prefix = plain_key[: cls.PREFIX_LENGTH] + token = cls.objects.filter(prefix=prefix).select_related("user").first() + + if token and check_password(plain_key, token.key_hash): + return token + + @classmethod + def regenerate(cls, user): + """Delete any existing token instance for the user and generate a new one.""" + cls.objects.filter(user=user).delete() + return cls.create_token(user) + + @classmethod + def revoke(cls, user): + """Delete any existing token instance for the user.""" + return cls.objects.filter(user=user).delete() + + +def get_api_token_model(): + """Return the concrete APIToken model from the API_TOKEN_MODEL setting.""" + try: + return django_apps.get_model(settings.API_TOKEN_MODEL) + except (ValueError, LookupError): + raise ImproperlyConfigured("API_TOKEN_MODEL is not properly defined.") diff --git a/aboutcode/api_auth/views.py b/aboutcode/api_auth/views.py new file mode 100644 index 00000000..4baa374c --- /dev/null +++ b/aboutcode/api_auth/views.py @@ -0,0 +1,55 @@ +# +# Copyright (c) nexB Inc. and others. All rights reserved. +# DejaCode is a trademark of nexB Inc. +# SPDX-License-Identifier: AGPL-3.0-only +# See https://github.com/aboutcode-org/dejacode for support or download. +# See https://aboutcode.org for more information about AboutCode FOSS projects. +# + +from django.contrib import messages +from django.core.exceptions import ImproperlyConfigured +from django.shortcuts import redirect +from django.utils.html import format_html +from django.views.generic import View + +from aboutcode.api_auth.models import get_api_token_model + + +class BaseAPIKeyActionView(View): + """Base view for API key management actions.""" + + success_url = None + success_message = "" + + def get_success_url(self): + if not self.success_url: + raise ImproperlyConfigured("No URL to redirect to. Provide a success_url.") + return str(self.success_url) + + def get_success_message(self, **kwargs): + if kwargs: + return format_html(self.success_message, **kwargs) + return self.success_message + + def post(self, request, *args, **kwargs): + raise NotImplementedError + + +class BaseGenerateAPIKeyView(BaseAPIKeyActionView): + """Generate a new API key and display it once via a success message.""" + + def post(self, request, *args, **kwargs): + token_model = get_api_token_model() + plain_key = token_model.regenerate(user=request.user) + messages.success(request, self.get_success_message(plain_key=plain_key)) + return redirect(self.get_success_url()) + + +class BaseRevokeAPIKeyView(BaseAPIKeyActionView): + """Revoke the current user's API key.""" + + def post(self, request, *args, **kwargs): + token_model = get_api_token_model() + token_model.revoke(user=request.user) + messages.success(request, self.get_success_message()) + return redirect(self.get_success_url()) diff --git a/api_auth-pyproject.toml b/api_auth-pyproject.toml new file mode 100644 index 00000000..363cdabc --- /dev/null +++ b/api_auth-pyproject.toml @@ -0,0 +1,44 @@ +[build-system] +requires = ["flot"] +build-backend = "flot.buildapi" + +[project] +name = "aboutcode.api_auth" +version = "0.2.0" +description = "" +license = { text = "Apache-2.0" } +readme = "aboutcode/api_auth/README.md" +requires-python = ">=3.11" +authors = [ { name = "nexB. Inc. and others", email = "info@aboutcode.org" } ] +keywords = [ + "open source", + "api", + "authentication", +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Topic :: Software Development", + "Topic :: Utilities", +] + +[project.urls] +Homepage = "https://github.com/aboutcode-org/dejacode" +Documentation = "https://dejacode.readthedocs.io/" +Repository = "https://github.com/aboutcode-org/dejacode/tree/main/aboutcode/api_auth" +Issues = "https://github.com/aboutcode-org/dejacode/issues" + +[tool.flot] +includes = [ + "aboutcode/api_auth/*", +] +metadata_files = [ + "LICENSE", + "NOTICE", +] diff --git a/component_catalog/admin.py b/component_catalog/admin.py index 3e96cf22..d88efaae 100644 --- a/component_catalog/admin.py +++ b/component_catalog/admin.py @@ -16,6 +16,7 @@ from django.urls import path from django.urls import reverse from django.utils.html import format_html +from django.utils.html import mark_safe from django.utils.http import urlencode from django.utils.translation import gettext_lazy as _ @@ -504,7 +505,7 @@ def response_change(self, request, obj): "This license change impacts component usage in a Product or in another " "Component.
{}".format(", ".join(changelist_links)) ) - self.message_user(request, format_html(msg), messages.WARNING) + self.message_user(request, mark_safe(msg), messages.WARNING) return response @@ -554,15 +555,23 @@ def get_actions(self, request): del actions["set_policy"] return actions - def log_deletion(self, request, object, object_repr): + def delete_model(self, request, obj): """ + Handle single object deletion from the delete view. Add the option to delete associated `Package` instances. - We use this method rather than `self.delete_model()` since we want to support both - the delete_view and the `delete_selected` action. """ - super().log_deletion(request, object, object_repr) if request.POST.get("delete_packages"): - object.packages.all().delete() + obj.packages.all().delete() + super().delete_model(request, obj) + + def delete_queryset(self, request, queryset): + """ + Handle bulk deletion from the delete_selected action. + Add the option to delete associated `Package` instances. + """ + if request.POST.get("delete_packages"): + Package.objects.filter(component__in=queryset).delete() + super().delete_queryset(request, queryset) def changeform_view(self, request, object_id=None, form_url="", extra_context=None): """ @@ -831,7 +840,7 @@ class PackageAdmin( "version", "qualifiers", "subpath", - "inferred_url", + "inferred_repo_url", ) }, ), @@ -884,6 +893,7 @@ class PackageAdmin( "parties", "datasource_id", "file_references", + "package_content", ) }, ), @@ -898,7 +908,7 @@ class PackageAdmin( ] readonly_fields = DataspacedAdmin.readonly_fields + ( "package_url", - "inferred_url", + "inferred_repo_url", ) form = PackageAdminForm importer_class = PackageImporter @@ -1052,7 +1062,7 @@ def collect_data_action(self, request, queryset): if not_updated: msg += f"
{not_updated} package(s) NOT updated (data already set or URL unavailable)" - self.message_user(request, format_html(msg), messages.SUCCESS) + self.message_user(request, mark_safe(msg), messages.SUCCESS) @admin.display( ordering="component", @@ -1067,11 +1077,11 @@ def components_links(self, obj): assigned_package.component.get_admin_link(target="_blank") for assigned_package in obj.componentassignedpackage_set.all() ] - return format_html("
".join(component_links)) + return mark_safe("
".join(component_links)) @admin.display(description="Inferred URL") def inferred_url(self, obj): - if inferred_url := obj.inferred_url: + if inferred_url := obj.inferred_repo_url: return urlize_target_blank(inferred_url) return "" diff --git a/component_catalog/api.py b/component_catalog/api.py index f90977c3..abd8705f 100644 --- a/component_catalog/api.py +++ b/component_catalog/api.py @@ -10,17 +10,19 @@ from django.db import transaction from django.forms.widgets import HiddenInput +from django.http import FileResponse +from django.http.response import StreamingHttpResponse -import coreapi -import coreschema import django_filters +import requests from packageurl.contrib import url2purl from packageurl.contrib.django.filters import PackageURLFilter from rest_framework import serializers +from rest_framework import status from rest_framework.decorators import action +from rest_framework.exceptions import APIException from rest_framework.fields import ListField from rest_framework.response import Response -from rest_framework.schemas import AutoSchema from component_catalog.admin import ComponentAdmin from component_catalog.admin import PackageAdmin @@ -56,8 +58,8 @@ from license_library.models import License from organization.api import OwnerEmbeddedSerializer from vulnerabilities.api import VulnerabilitySerializer -from vulnerabilities.filters import RISK_SCORE_RANGES from vulnerabilities.filters import ScoreRangeFilter +from vulnerabilities.models import RISK_SCORE_RANGES class LicenseSummaryMixin: @@ -618,6 +620,7 @@ class PackageSerializer( required=False, scope_content_type=True, ) + package_content = serializers.ReadOnlyField(source="get_package_content_display") collect_data = serializers.BooleanField( write_only=True, required=False, @@ -687,6 +690,7 @@ class Meta: "parties", "datasource_id", "file_references", + "package_content", "external_references", "created_date", "last_modified_date", @@ -867,6 +871,21 @@ def collect_create_scan(download_url, user): return package +class ScanCodeUnavailable(APIException): + status_code = status.HTTP_400_BAD_REQUEST + default_detail = "The ScanCode.io service is not available" + + +class ScanDataUnavailable(APIException): + status_code = status.HTTP_400_BAD_REQUEST + default_detail = "Scan data is not available" + + +class ScanFetchError(APIException): + status_code = status.HTTP_400_BAD_REQUEST + default_detail = "Could not fetch scan data" + + class PackageViewSet( SendAboutFilesMixin, AboutCodeFilesActionMixin, @@ -922,24 +941,86 @@ def about(self, request, uuid): package = self.get_object() return Response({"about_data": package.as_about_yaml()}) - download_url_description = ( - "A single, or list of, Download URL(s).

" - 'cURL style: -d "download_url=url1&download_url=url2"

' - 'Python: data = {"download_url": ["url1", "url2"]}' - ) + def _get_scancodeio_project_info(self, scancodeio, package): + if not scancodeio.is_available(): + raise ScanCodeUnavailable() - add_action_schema = AutoSchema( - manual_fields=[ - coreapi.Field( - "download_url", - required=True, - location="body", - schema=coreschema.String(description=download_url_description), - ), - ] - ) + project_info = scancodeio.get_project_info(download_url=package.download_url) + if not project_info: + raise ScanDataUnavailable() + + return project_info + + @action(detail=True, name="Scan informations") + def scan_info(self, request, uuid): + """Return information about the scan from ScanCode.io.""" + package = self.get_object() + dataspace = request.user.dataspace + scancodeio = ScanCodeIO(dataspace) + project_info = self._get_scancodeio_project_info(scancodeio, package) + + return Response(project_info) + + @action(detail=True, name="Scan results") + def scan_results(self, request, uuid): + """ + Stream scan results directly from ScanCode.io back to the client. + + The response body is not loaded in memory but proxied chunk by chunk, + making it suitable for large scan result payloads. + """ + package = self.get_object() + dataspace = request.user.dataspace + scancodeio = ScanCodeIO(dataspace) + project_info = self._get_scancodeio_project_info(scancodeio, package) + + project_uuid = project_info.get("uuid") + scan_results_url = scancodeio.get_scan_action_url(project_uuid, "results") + + try: + scan_response = scancodeio.stream_scan_data(scan_results_url) + except requests.RequestException: + raise ScanFetchError() + + return StreamingHttpResponse( + scan_response.iter_content(chunk_size=8192), + content_type=scan_response.headers.get("Content-Type", "application/json"), + ) + + @action(detail=True, name="Scan summary") + def scan_summary(self, request, uuid): + """Return the scan summary from ScanCode.io.""" + package = self.get_object() + dataspace = request.user.dataspace + scancodeio = ScanCodeIO(dataspace) + project_info = self._get_scancodeio_project_info(scancodeio, package) + + project_uuid = project_info.get("uuid") + scan_summary_url = scancodeio.get_scan_action_url(project_uuid, "summary") + scan_summary = scancodeio.fetch_scan_data(scan_summary_url) + + return Response(scan_summary) + + @action(detail=True, name="Scan data (download as .zip)") + def scan_data_download_zip(self, request, uuid): + """Download scan data: results and summary, as a zip file.""" + package = self.get_object() + dataspace = request.user.dataspace + scancodeio = ScanCodeIO(dataspace) + project_info = self._get_scancodeio_project_info(scancodeio, package) + + project_uuid = project_info.get("uuid") + filename = package.filename or package.package_url_filename + + scan_data_as_zip = scancodeio.scan_data_as_zip(project_uuid, filename) + return FileResponse( + scan_data_as_zip, + filename=f"{filename}_scan.zip", + as_attachment=True, + content_type="application/zip", + ) - @action(detail=False, methods=["post"], name="Package Add", schema=add_action_schema) + @action(detail=False, methods=["post"], name="Package Add") def add(self, request): """ Alternative way to add a package providing only its `download_url`. diff --git a/component_catalog/filters.py b/component_catalog/filters.py index ecfa5f7e..5e08ae09 100644 --- a/component_catalog/filters.py +++ b/component_catalog/filters.py @@ -205,6 +205,13 @@ class PackageFilterSet(DataspacedFilterSet): "sha1", "md5", ], + ordering=[ + "type", + "namespace", + "name", + # In a search context, we want to display the most recent versions first + "-version", + ], search_fields=[ "type", "namespace", diff --git a/component_catalog/forms.py b/component_catalog/forms.py index 39430444..abc22e9f 100644 --- a/component_catalog/forms.py +++ b/component_catalog/forms.py @@ -339,6 +339,7 @@ class Meta: "version", "qualifiers", "subpath", + "package_content", "collect_data", ] widgets = { @@ -407,7 +408,7 @@ def helper(self): HTML("
"), Group("description", "keywords"), Group("primary_language", "cpe"), - Group("size", "release_date"), + Group("package_content", "size", "release_date"), Group("dependencies", "notes"), HTML("
"), Group("homepage_url", "code_view_url"), @@ -490,10 +491,22 @@ class Meta: "copyright", "primary_language", "description", + "download_url", + "repository_download_url", "homepage_url", + "repository_homepage_url", + "bug_tracking_url", + "code_view_url", + "vcs_url", + "api_data_url", + "size", "release_date", "notice_text", "dependencies", + "md5", + "sha1", + "sha256", + "sha512", ] widgets = { "copyright": forms.Textarea(attrs={"rows": 2}), @@ -624,8 +637,11 @@ def __init__(self, user, *args, **kwargs): super().__init__(user, *args, **kwargs) product_field = self.fields["product"] - perms = ["view_product", "change_product"] - product_field.queryset = Product.objects.get_queryset(user, perms=perms) + product_field.queryset = Product.objects.get_queryset( + user=user, + perms=["view_product", "change_product"], + exclude_locked=True, + ) if relation_instance: help_text = f'"{relation_instance}" will be assigned to the selected product.' @@ -692,7 +708,7 @@ class AddToProductAdminForm(forms.Form): ids = forms.CharField(widget=forms.widgets.HiddenInput) replace_existing_version = forms.BooleanField( required=False, - initial=False, + initial=True, label="Replace existing relationships by newer version.", help_text=( "Select this option to replace any existing relationships with a different version " @@ -709,7 +725,9 @@ def __init__(self, request, model, relation_model, *args, **kwargs): self.relation_model = relation_model self.dataspace = request.user.dataspace self.fields["product"].queryset = Product.objects.get_queryset( - request.user, perms=["view_product", "change_product"] + user=request.user, + perms=["view_product", "change_product"], + exclude_locked=True, ) def get_selected_objects(self): @@ -1166,6 +1184,7 @@ class Meta: "version", "qualifiers", "subpath", + "package_content", ] diff --git a/component_catalog/license_expression_dje.py b/component_catalog/license_expression_dje.py index b2f0e58e..b179b599 100644 --- a/component_catalog/license_expression_dje.py +++ b/component_catalog/license_expression_dje.py @@ -14,7 +14,7 @@ from django.forms import widgets from django.urls import reverse from django.utils.html import format_html -from django.utils.safestring import mark_safe +from django.utils.html import mark_safe from boolean.boolean import PARSE_ERRORS from license_expression import ExpressionError @@ -149,7 +149,7 @@ def normalize_and_validate_expression( msg = str(ee) if include_available: msg += available_licenses_message(licensing) - raise ValidationError(format_html(msg), code="invalid") + raise ValidationError(mark_safe(msg), code="invalid") except ParseError as pe: msg = PARSE_ERRORS[pe.error_code] @@ -157,15 +157,15 @@ def normalize_and_validate_expression( msg += ": " + pe.token_string if include_available: msg += available_licenses_message(licensing) - raise ValidationError(format_html(msg), code="invalid") + raise ValidationError(mark_safe(msg), code="invalid") except (ValueError, TypeError) as ve: msg = "Invalid reference licenses data.\n" + str(ve) - raise ValidationError(format_html(msg), code="invalid") + raise ValidationError(mark_safe(msg), code="invalid") except Exception as e: msg = "Invalid license expression.\n" + str(e) - raise ValidationError(format_html(msg), code="invalid") + raise ValidationError(mark_safe(msg), code="invalid") # NOTE: we test for None because an expression cannot be resolved to # a boolean and a plain "if parsed" would attempt to resolve the diff --git a/component_catalog/management/commands/componentfrompackage.py b/component_catalog/management/commands/componentfrompackage.py index 101ce07a..7d877b66 100644 --- a/component_catalog/management/commands/componentfrompackage.py +++ b/component_catalog/management/commands/componentfrompackage.py @@ -101,7 +101,7 @@ def create_component_from_package(self, package): # The proper policy will be set from the ``license_expression`` value component_data.pop("usage_policy", None) - if inferred_url := package.inferred_url: + if inferred_url := package.inferred_repo_url: component_data["code_view_url"] = inferred_url component_data["homepage_url"] = inferred_url diff --git a/component_catalog/migrations/0012_alter_component_children.py b/component_catalog/migrations/0012_alter_component_children.py new file mode 100644 index 00000000..28701065 --- /dev/null +++ b/component_catalog/migrations/0012_alter_component_children.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.4 on 2025-07-30 13:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('component_catalog', '0011_alter_component_owner'), + ] + + operations = [ + migrations.AlterField( + model_name='component', + name='children', + field=models.ManyToManyField(through='component_catalog.Subcomponent', through_fields=('parent', 'child'), to='component_catalog.component'), + ), + ] diff --git a/component_catalog/migrations/0013_package_package_content.py b/component_catalog/migrations/0013_package_package_content.py new file mode 100644 index 00000000..ccf667de --- /dev/null +++ b/component_catalog/migrations/0013_package_package_content.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.8 on 2025-11-24 12:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('component_catalog', '0012_alter_component_children'), + ] + + operations = [ + migrations.AddField( + model_name='package', + name='package_content', + field=models.IntegerField(blank=True, choices=[(1, 'curation'), (2, 'patch'), (3, 'source_repo'), (4, 'source_archive'), (5, 'binary'), (6, 'test'), (7, 'doc')], help_text='Content of this Package as one of: curation, patch, source_repo, source_archive, binary, test, doc', null=True), + ), + ] diff --git a/component_catalog/models.py b/component_catalog/models.py index 2d01c961..55902b65 100644 --- a/component_catalog/models.py +++ b/component_catalog/models.py @@ -18,15 +18,20 @@ from django.core.exceptions import ValidationError from django.core.validators import EMPTY_VALUES from django.db import models +from django.db.models import Case from django.db.models import CharField from django.db.models import Count from django.db.models import Exists +from django.db.models import F from django.db.models import OuterRef +from django.db.models import Value +from django.db.models import When from django.db.models.functions import Concat from django.dispatch import receiver from django.template.defaultfilters import filesizeformat from django.utils.functional import cached_property from django.utils.html import format_html +from django.utils.html import mark_safe from django.utils.text import format_lazy from django.utils.text import get_valid_filename from django.utils.text import normalize_newlines @@ -50,11 +55,11 @@ from component_catalog.license_expression_dje import get_license_objects from component_catalog.license_expression_dje import parse_expression from component_catalog.license_expression_dje import render_expression_as_html +from dejacode_toolkit import download from dejacode_toolkit import spdx -from dejacode_toolkit.download import DataCollectionException -from dejacode_toolkit.download import collect_package_data from dejacode_toolkit.purldb import PurlDB from dejacode_toolkit.purldb import pick_purldb_entry +from dejacode_toolkit.purldb import pick_source_package from dejacode_toolkit.scancodeio import ScanCodeIO from dje import urn from dje.copier import post_copy @@ -72,6 +77,8 @@ from dje.models import ReferenceNotesMixin from dje.tasks import logger as tasks_logger from dje.utils import is_purl_str +from dje.utils import merge_common_non_empty_values +from dje.utils import plain_purls_equal from dje.utils import set_fields_from_object from dje.validators import generic_uri_validator from dje.validators import validate_url_segment @@ -184,7 +191,7 @@ def get_license_expression(self, template="{symbol.key}", as_link=False, show_po as_link=as_link, show_policy=show_policy, ) - return format_html(rendered) + return mark_safe(rendered) def get_license_expression_attribution(self): # note: the fields use in the template must be available as attributes or @@ -1647,14 +1654,113 @@ def __str__(self): return self.label +class PackageContentFieldMixin(models.Model): + """ + Field extracted from the `purldb.packagedb.models.Package` model. + It need to stay aligned with its upstream PurlDB implementation. + """ + + class PackageContentType(models.IntegerChoices): + CURATION = 1, "curation" + PATCH = 2, "patch" + SOURCE_REPO = 3, "source_repo" + SOURCE_ARCHIVE = 4, "source_archive" + BINARY = 5, "binary" + TEST = 6, "test" + DOC = 7, "doc" + + package_content = models.IntegerField( + null=True, + blank=True, + choices=PackageContentType.choices, + help_text=_( + "Content of this Package as one of: {}".format(", ".join(PackageContentType.labels)) + ), + ) + + class Meta: + abstract = True + + @classmethod + def get_package_content_value_from_label(cls, label): + """Convert a package_content string label to its integer value.""" + try: + return cls.PackageContentType[label.upper()].value + except (KeyError, AttributeError): + return + + PACKAGE_URL_FIELDS = ["type", "namespace", "name", "version", "qualifiers", "subpath"] +def get_plain_package_url_expression(): + """ + Return a Django expression to compute the "PLAIN" Package URL (PURL). + Return an empty string if the required `type` or `name` values are missing. + """ + plain_package_url = Concat( + Value("pkg:"), + F("type"), + Case( + When(namespace="", then=Value("")), + default=Concat(Value("/"), F("namespace")), + output_field=CharField(), + ), + Value("/"), + F("name"), + Case( + When(version="", then=Value("")), + default=Concat(Value("@"), F("version")), + output_field=CharField(), + ), + output_field=CharField(), + ) + + return Case( + When(type="", then=Value("")), + When(name="", then=Value("")), + default=plain_package_url, + output_field=CharField(), + ) + + +def get_package_url_expression(): + """ + Return a Django expression to compute the "FULL" Package URL (PURL). + Return an empty string if the required `type` or `name` values are missing. + """ + package_url = Concat( + get_plain_package_url_expression(), + Case( + When(qualifiers="", then=Value("")), + default=Concat(Value("?"), F("qualifiers")), + output_field=CharField(), + ), + Case( + When(subpath="", then=Value("")), + default=Concat(Value("#"), F("subpath")), + output_field=CharField(), + ), + output_field=CharField(), + ) + + return Case( + When(type="", then=Value("")), + When(name="", then=Value("")), + default=package_url, + output_field=CharField(), + ) + + class PackageQuerySet(PackageURLQuerySetMixin, VulnerabilityQuerySetMixin, DataspacedQuerySet): def has_package_url(self): """Return objects with Package URL defined.""" return self.filter(~models.Q(type="") & ~models.Q(name="")) + def has_download_url(self): + """Return objects with download URL defined.""" + return self.filter(~models.Q(download_url="")) + def annotate_sortable_identifier(self): """ Annotate the QuerySet with a `sortable_identifier` value that combines @@ -1665,6 +1771,26 @@ def annotate_sortable_identifier(self): sortable_identifier=Concat(*PACKAGE_URL_FIELDS, "filename", output_field=CharField()) ) + def annotate_plain_package_url(self): + """ + Annotate the QuerySet with a computed 'plain' Package URL (PURL). + + This plain PURL is a simplified version that includes only the core fields: + `type`, `namespace`, `name`, and `version`. It omits any qualifiers or + subpath components, providing a normalized and minimal representation + of the Package URL. + """ + return self.annotate(plain_purl=get_plain_package_url_expression()) + + def annotate_package_url(self): + """ + Annotate the QuerySet with a fully-computed Package URL (PURL). + + This includes the core PURL fields (`type`, `namespace`, `name`, `version`) + as well as any qualifiers and subpath components. + """ + return self.annotate(purl=get_package_url_expression()) + def only_rendering_fields(self): """Minimum requirements to render a Package element in the UI.""" return self.only( @@ -1707,6 +1833,7 @@ class Package( URLFieldsMixin, HashFieldsMixin, PackageURLMixin, + PackageContentFieldMixin, DataspacedModel, ): filename = models.CharField( @@ -1951,10 +2078,15 @@ def package_url_filename(self): return get_valid_filename(cleaned_package_url) @property - def inferred_url(self): - """Return the URL deduced from the information available in a Package URL (purl).""" + def inferred_repo_url(self): + """Return the repo URL deduced from the Package URL (purl).""" return purl2url.get_repo_url(self.package_url) + def infer_download_url(self): + """Infer the download URL deduced from the Package URL (purl).""" + if self.package_url: + return download.infer_download_url(self.package_url) + def get_url(self, name, params=None, include_identifier=False): if not params: params = [self.dataspace.name, quote_plus(str(self.uuid))] @@ -2037,8 +2169,8 @@ def collect_data(self, force_update=False, save=True): return try: - package_data = collect_package_data(self.download_url) - except DataCollectionException as e: + package_data = download.collect_package_data(self.download_url) + except download.DataCollectionException as e: tasks_logger.info(e) return tasks_logger.info("Package data collected.") @@ -2391,7 +2523,7 @@ def create_from_url(cls, url, user): scoped_packages_qs = cls.objects.scope(user.dataspace) if is_purl_str(url): - download_url = purl2url.get_download_url(url) + download_url = download.infer_download_url(url) package_url = PackageURL.from_string(url) existing_packages = scoped_packages_qs.for_package_url(url, exact_match=True) else: @@ -2406,24 +2538,29 @@ def create_from_url(cls, url, user): ) # Matching in PurlDB early to avoid more processing in case of a match. - purldb_data = None + purldb_entry = None if user.dataspace.enable_purldb_access: package_for_match = cls(download_url=download_url) package_for_match.set_package_url(package_url) purldb_entries = package_for_match.get_purldb_entries(user) - # Look for one ith the same exact purl in that case - if purldb_data := pick_purldb_entry(purldb_entries, purl=url): - # The format from PurlDB is "2019-11-18T00:00:00Z" from DateTimeField - if release_date := purldb_data.get("release_date"): - purldb_data["release_date"] = release_date.split("T")[0] - package_data.update(purldb_data) - - if download_url and not purldb_data: - package_data = collect_package_data(download_url) - - if sha1 := package_data.get("sha1"): - if sha1_match := scoped_packages_qs.filter(sha1=sha1): - package_link = sha1_match[0].get_absolute_link() + # Look for one with the same exact purl in that case + if purldb_entry := pick_purldb_entry(purldb_entries, purl=url): + cls.clean_purldb_entry(purldb_entry) + package_data.update(purldb_entry) + + if download_url and not purldb_entry: + package_data = download.collect_package_data(download_url) + + # Check for existing package by hash fields with a single database query + hash_fields = ["sha512", "sha256", "sha1", "md5"] + hash_filters = models.Q() + for hash_field in hash_fields: + if hash_value := package_data.get(hash_field): + hash_filters |= models.Q(**{hash_field: hash_value}) + + if hash_filters: + if package_match := scoped_packages_qs.filter(hash_filters).first(): + package_link = package_match.get_absolute_link() raise PackageAlreadyExistsWarning( f"{url} already exists in your Dataspace as {package_link}" ) @@ -2442,10 +2579,10 @@ def get_purldb_entries(self, user, max_request_call=0, timeout=10): """ Return the PurlDB entries that correspond to this Package instance. - Matching on the following fields order: - - Package URL - - SHA1 - - Download URL + Matching is performed in order of decreasing accuracy: + 1. Hash - Most accurate, matches exact file content + 2. Download URL - High accuracy, matches specific package source + 3. Package URL - Broadest match, may return multiple versions/variants A `max_request_call` integer can be provided to limit the number of HTTP requests made to the PackageURL server. @@ -2454,37 +2591,114 @@ def get_purldb_entries(self, user, max_request_call=0, timeout=10): is nothing was found. """ payloads = [] + purldb_entries = [] package_url = self.package_url - if package_url: - payloads.append({"purl": package_url}) + if self.sha256: + payloads.append({"sha256": self.sha256}) if self.sha1: payloads.append({"sha1": self.sha1}) + if self.md5: + payloads.append({"md5": self.md5}) if self.download_url: payloads.append({"download_url": self.download_url}) + if package_url: + payloads.append({"purl": package_url}) purldb = PurlDB(user.dataspace) for index, payload in enumerate(payloads): if max_request_call and index >= max_request_call: return - if packages_data := purldb.find_packages(payload, timeout): - return packages_data + if purldb_entries := purldb.find_packages(payload, timeout): + break + + if not purldb_entries: + return [] + + # Cleanup the PurlDB entries: + # Packages with different "plain" PURL are excluded. + # The qualifiers and subpaths are not involved in this comparison. + if package_url: + purldb_entries = [ + entry + for entry in purldb_entries + if plain_purls_equal(entry.get("purl"), package_url) + ] + + return purldb_entries + + @classmethod + def normalize_purldb_release_date(cls, data): + """Strip the time portion from a PurlDB DateTimeField value.""" + if release_date := data.get("release_date"): + data["release_date"] = release_date.split("T")[0] + + @classmethod + def convert_purldb_package_content_label(cls, data): + """Convert package_content from a string label to its integer value in place.""" + if content_label := data.get("package_content"): + if content_value := Package.get_package_content_value_from_label(content_label): + data["package_content"] = content_value + + @classmethod + def clean_purldb_entry(cls, data): + """Normalize PurlDB entry data for local use.""" + cls.normalize_purldb_release_date(data) + cls.convert_purldb_package_content_label(data) + # Set the declared_license_expression as the "concluded" license_expression + data["license_expression"] = data.get("declared_license_expression") def update_from_purldb(self, user): """ - Find this Package in the PurlDB and update empty fields with PurlDB data - when available. + Update this Package instance with data from PurlDB. + + - Retrieves matching entries from PurlDB using the given user. + - If exactly one match is found, its data is used directly. + - If multiple entries are found, leverage the package_content value when + available to select a "source" package. + - If multiple entries are found, only values that are non-empty and + common across all entries are merged and used to update the Package. """ purldb_entries = self.get_purldb_entries(user) if not purldb_entries: return - package_data = purldb_entries[0] - # The format from PURLDB is "2019-11-18T00:00:00Z" - if release_date := package_data.get("release_date"): - package_data["release_date"] = release_date.split("T")[0] - package_data["license_expression"] = package_data.get("declared_license_expression") + purldb_entries_count = len(purldb_entries) + if purldb_entries_count == 1: + package_data = purldb_entries[0] + elif source_package := pick_source_package(purldb_entries): + package_data = source_package + else: + package_data = merge_common_non_empty_values(purldb_entries) + + self.clean_purldb_entry(package_data) + + # Avoid raising an IntegrityError when the values in `package_data` for the + # identifier fields already exist on another Package instance. + # + # This situation can occur when a complete package (with both `purl` and + # `download_url`) already exists in the Dataspace, and `update_from_purldb` is + # called on a different package that has the same `purl` but no `download_url`. + # + # If we try to assign the same `download_url` to the second package, it would + # violate the unique constraints defined in the Package model (since the + # combination of fields must be unique). + unique_filters_lookups = { + field_name: package_data.get(field_name, "") + for field_name in self.get_identifier_fields() + } + unique_filters_qs = ( + Package.objects.scope(self.dataspace) + .filter(**unique_filters_lookups) + .exclude(pk=self.pk) + ) + if unique_filters_qs.exists(): + # Remove the problematic "identifier_fields" values and the checksum values + hash_field_names = [field.name for field in HashFieldsMixin._meta.fields] + identifier_fields = self.get_identifier_fields() + for field_name in [*hash_field_names, *identifier_fields]: + package_data.pop(field_name, None) updated_fields = self.update_from_data( user, @@ -2492,21 +2706,62 @@ def update_from_purldb(self, user): override=False, override_unknown=True, ) + + if updated_fields: + msg = f"Automatically updated {', '.join(updated_fields)} from PurlDB." + logger.debug(f"PurlDB: {msg}") + History.log_change(user, self, message=msg) + return updated_fields - def update_from_scan(self, user): - scancodeio = ScanCodeIO(self.dataspace) + def update_from_scan(self, user, update_products=False): + package = self + dataspace = self.dataspace + scancodeio = ScanCodeIO(dataspace) + can_update_from_scan = all( [ - self.dataspace.enable_package_scanning, - self.dataspace.update_packages_from_scan, + dataspace.enable_package_scanning, + dataspace.update_packages_from_scan, scancodeio.is_configured(), ] ) + if not can_update_from_scan: + return + + updated_fields = scancodeio.update_from_scan(package=package, user=user) + + if update_products: + if "declared_license_expression" in updated_fields: + package.productpackages.update_license_unknown() + + return updated_fields - if can_update_from_scan: - updated_fields = scancodeio.update_from_scan(package=self, user=user) - return updated_fields + def get_related_packages_qs(self): + """ + Return a QuerySet of packages that are considered part of the same + "Package Set". + + A "Package Set" consists of all packages that share the same "plain" + Package URL (PURL), meaning they have identical values for the following PURL + components: + `type`, `namespace`, `name`, and `version`. + The `qualifiers` and `subpath` components are ignored for this comparison. + """ + plain_package_url = self.plain_package_url + if not plain_package_url: + return None + + return ( + self.__class__.objects.scope(self.dataspace) + .for_package_url(plain_package_url, exact_match=True) + .order_by( + *PACKAGE_URL_FIELDS, + "filename", + "download_url", + ) + .distinct() + ) class PackageAssignedLicense(DataspacedModel): diff --git a/component_catalog/templates/component_catalog/base_component_package_list.html b/component_catalog/templates/component_catalog/base_component_package_list.html index 8d1617de..436d83d9 100644 --- a/component_catalog/templates/component_catalog/base_component_package_list.html +++ b/component_catalog/templates/component_catalog/base_component_package_list.html @@ -3,7 +3,7 @@ {% block top-right-buttons %} - @@ -47,43 +47,42 @@ {% endif %} diff --git a/component_catalog/templates/component_catalog/includes/add_to.js.html b/component_catalog/templates/component_catalog/includes/add_to.js.html index a024fe67..addd795f 100644 --- a/component_catalog/templates/component_catalog/includes/add_to.js.html +++ b/component_catalog/templates/component_catalog/includes/add_to.js.html @@ -4,7 +4,7 @@ let add_to_btn_wrapper = add_to_btn.parentElement; let handle_button_display = function () { - let checkedCheckboxes = document.querySelectorAll('main input[type="checkbox"]:checked'); + let checkedCheckboxes = document.querySelectorAll('#object-list-table input[type="checkbox"]:checked'); if (checkedCheckboxes.length >= 1) { add_to_btn.classList.remove('disabled'); add_to_btn_wrapper.setAttribute('data-bs-original-title', ''); @@ -15,7 +15,7 @@ }; // Adding change event listener to all checkboxes - document.querySelectorAll('main input[type="checkbox"]').forEach(function(checkbox) { + document.querySelectorAll('#object-list-table input[type="checkbox"]').forEach(function(checkbox) { checkbox.addEventListener('change', handle_button_display); }); @@ -27,35 +27,40 @@ handle_button_display(); }); - $('#add-to-product-modal, #add-to-component-modal').on('show.bs.modal', function (event) { - // Do not include the select-all as its value is not an id we want to keep - let checked = $('main input[type="checkbox"]:checked').not('#checkbox-select-all'); + document.querySelectorAll('#add-to-product-modal, #add-to-component-modal').forEach(function(modal) { + modal.addEventListener('show.bs.modal', function (event) { + // Do not include the select-all as its value is not an id we want to keep + let checked = Array.from(document.querySelectorAll('#object-list-table input[type="checkbox"]:checked')) + .filter(checkbox => checkbox.id !== 'checkbox-select-all'); - if (checked.length < 1) { - event.preventDefault(); - return false; - } + if (checked.length < 1) { + event.preventDefault(); + return false; + } - let ids_input = $(this).find('#id_ids'); - let object_repr_list = $(this).find('#object-repe-list'); + let ids_input = modal.querySelector('#id_ids'); + let object_repr_list = modal.querySelector('#object-repr-list'); - if (ids_input) { - let selected_ids = []; - object_repr_list.html(''); + if (ids_input) { + let selected_ids = []; + object_repr_list.innerHTML = ''; - checked.each(function() { - selected_ids.push(this.value); - $('
  • ').text($(this).data('object-repr')).appendTo(object_repr_list); - }); + checked.forEach(function(checkbox) { + selected_ids.push(checkbox.value); + let li = document.createElement('li'); + li.textContent = checkbox.dataset.objectRepr; + object_repr_list.appendChild(li); + }); - ids_input.val(selected_ids); + ids_input.value = selected_ids.join(','); - let new_component_link = $('#new-component-link'); - if (new_component_link) { - let new_component_url = new_component_link.data('add-url') + '?package_ids=' + selected_ids.join(); - new_component_link.attr('href', new_component_url); + let new_component_link = document.getElementById('new-component-link'); + if (new_component_link) { + let new_component_url = new_component_link.dataset.addUrl + '?package_ids=' + selected_ids.join(','); + new_component_link.setAttribute('href', new_component_url); + } } - } + }); }); // Select all forms with id starting with "add-to-" diff --git a/component_catalog/templates/component_catalog/includes/add_to_modal.html b/component_catalog/templates/component_catalog/includes/add_to_modal.html index 847f1758..795d3d98 100644 --- a/component_catalog/templates/component_catalog/includes/add_to_modal.html +++ b/component_catalog/templates/component_catalog/includes/add_to_modal.html @@ -13,7 +13,7 @@ {% if request.resolver_match.url_name|default:""|slice:"-4:" == "list" %}
    Selected objects:
    -
      +
        {% endif %} diff --git a/component_catalog/templates/component_catalog/includes/scan_progress.html b/component_catalog/templates/component_catalog/includes/scan_progress.html index d609fc66..7d43a928 100644 --- a/component_catalog/templates/component_catalog/includes/scan_progress.html +++ b/component_catalog/templates/component_catalog/includes/scan_progress.html @@ -1,22 +1,5 @@ -{{ scan.runs.0.status|title }} -{% include 'component_catalog/includes/scan_status.html' with status=scan.runs.0.status only %} - -{% if compact_mode %} -
        - {% if view_url %} -   - {% endif %} - {% if scan.download_result_url %} - - {% endif %} -
        -{% else %} -
        - {% if scan.download_result_url %} - Results - {% endif %} - {% if scan.delete_url %} - - {% endif %} -
        -{% endif %} \ No newline at end of file +{{ scan.status_for_display }} +{% include 'component_catalog/includes/scan_status.html' with status=scan.status only %} +
        + {% include 'component_catalog/includes/scan_actions_list.html' with scan=scan only %} +
        \ No newline at end of file diff --git a/component_catalog/templates/component_catalog/includes/scan_status.html b/component_catalog/templates/component_catalog/includes/scan_status.html index 2bcad92e..77f6ac47 100644 --- a/component_catalog/templates/component_catalog/includes/scan_status.html +++ b/component_catalog/templates/component_catalog/includes/scan_status.html @@ -1,13 +1,13 @@ -
        +
        {% if status == 'success' %} -
        +
        {% elif status == 'failure' or status == "stopped" or status == "stale" %} -
        +
        {% elif status == 'warning' %} -
        +
        {% elif status == 'running' %} -
        +
        {% elif status == 'not_started' or status == 'queued' %} -
        +
        {% endif %}
        \ No newline at end of file diff --git a/component_catalog/templates/component_catalog/includes/scan_delete_modal.html b/component_catalog/templates/component_catalog/modals/scan_delete_modal.html similarity index 93% rename from component_catalog/templates/component_catalog/includes/scan_delete_modal.html rename to component_catalog/templates/component_catalog/modals/scan_delete_modal.html index a2cc0d88..a896132c 100644 --- a/component_catalog/templates/component_catalog/includes/scan_delete_modal.html +++ b/component_catalog/templates/component_catalog/modals/scan_delete_modal.html @@ -10,7 +10,7 @@

        Are you sure you want to delete this Scan?

        All data and results related to the Scan will be deleted.
        - If the Scan is in progress, it will be cancel. + If the Scan is in progress, it will be canceled.

        + + + {# Security panel #} +
        +
        + {# Header #} +
        +

        {% trans "Security compliance" %}

        +
        + + {% if vulnerability_count == 0 or above_threshold_count == 0 %} + {% trans "OK" %} + {% elif max_vulnerability_severity == "critical" %} + {% trans "Critical" %} + {% elif max_vulnerability_severity == "high" %} + {% trans "High" %} + {% elif max_vulnerability_severity == "medium" %} + {% trans "Medium" %} + {% else %} + {% trans "Low" %} + {% endif %} +
        +
        + + {# Summary #} + {% if vulnerability_count > 0 %} +

        + {% if risk_threshold_number %} + {% if above_threshold_count > 0 %} + {{ above_threshold_count }} {% trans "of" %} {{ vulnerability_count }} + {% trans "vulnerabilit" %}{{ vulnerability_count|pluralize:"y,ies" }} + {% trans "above risk threshold of" %} {{ risk_threshold_number }} + {% else %} + {{ vulnerability_count }} + {% trans "vulnerabilit" %}{{ vulnerability_count|pluralize:"y,ies" }}, + {% trans "all below risk threshold of" %} {{ risk_threshold_number }} + {% endif %} + {% else %} + {{ vulnerability_count }} + {% trans "vulnerabilit" %}{{ vulnerability_count|pluralize:"y,ies" }} + — {% trans "no risk threshold set" %} + {% endif %} +

        + {% endif %} + + {# Vulnerability list #} + {% for vulnerability in vulnerabilities %} +
        + + {% if vulnerability.risk_level == "critical" %} + {% trans "Critical" %} + {% elif vulnerability.risk_level == "high" %} + {% trans "High" %} + {% elif vulnerability.risk_level == "medium" %} + {% trans "Medium" %} + {% elif vulnerability.risk_level == "low" %} + {% trans "Low" %} + {% else %} + {% trans "Unknown" %} + {% endif %} + + + {{ vulnerability.vulnerability_id }} + + {{ vulnerability.summary|truncatechars:70 }} +
        + {% empty %} +
        + + {% trans "No known vulnerabilities" %} +
        + {% endfor %} + + {# View all link #} + {% if vulnerabilities|length < vulnerability_count %} + + {% endif %} +
        +
        + \ No newline at end of file diff --git a/product_portfolio/templates/product_portfolio/compliance/metric_cards.html b/product_portfolio/templates/product_portfolio/compliance/metric_cards.html new file mode 100644 index 00000000..05a4ecba --- /dev/null +++ b/product_portfolio/templates/product_portfolio/compliance/metric_cards.html @@ -0,0 +1,78 @@ +{% load i18n humanize %} +
        +
        +
        +
        {% trans "Total packages" %}
        +
        {{ total_packages|intcomma }}
        +
        + {% if package_issues_count == 0 %} + {% trans "No policy violations" %} + {% else %} + + {{ package_issues_count }} {% trans "package policy violation" %}{{ package_issues_count|pluralize }} + + {% endif %} +
        +
        +
        +
        +
        +
        {% trans "License compliance" %}
        +
        + {{ license_compliance_pct }}% +
        +
        + {% if packages_with_license_issues == 0 %} + {% trans "All packages within policy" %} + {% else %} + + {{ packages_with_license_issues }} {% trans "packages with license policy violations" %} + + {% endif %} +
        +
        +
        +
        +
        +
        {% trans "License coverage" %}
        +
        + {{ license_coverage_pct }}% +
        +
        + {% if license_coverage_pct == 100 %} + {% trans "All packages have a license" %} + {% else %} + + {{ package_without_license_count }} {% trans "packages without license" %} + + {% endif %} +
        +
        +
        +
        +
        +
        {% trans "Vulnerabilities" %}
        + {% if vulnerability_count == 0 %} +
        0
        +
        {% trans "No known vulnerabilities" %}
        + {% elif risk_threshold_number and above_threshold_count == 0 %} +
        0
        +
        + {{ vulnerability_count }} {% trans "below risk threshold of" %} {{ risk_threshold_number }} +
        + {% elif risk_threshold_number %} +
        {{ above_threshold_count }}
        +
        + {% trans "of" %} {{ vulnerability_count }} {% trans "above threshold of" %} {{ risk_threshold_number }} +
        + {% else %} +
        + {{ vulnerability_count }} +
        +
        + {% if critical_count %}{{ critical_count }} {% trans "critical" %}{% endif %}{% if critical_count and high_count %}, {% endif %}{% if high_count %}{{ high_count }} {% trans "high" %}{% endif %}{% if medium_count and critical_count or medium_count and high_count %}, {% endif %}{% if medium_count %}{{ medium_count }} {% trans "medium" %}{% endif %}{% if low_count and vulnerability_count == low_count %}{{ low_count }} {% trans "low" %}{% endif %} +
        + {% endif %} +
        +
        +
        \ No newline at end of file diff --git a/product_portfolio/templates/product_portfolio/compliance_dashboard.html b/product_portfolio/templates/product_portfolio/compliance_dashboard.html new file mode 100644 index 00000000..4f393b08 --- /dev/null +++ b/product_portfolio/templates/product_portfolio/compliance_dashboard.html @@ -0,0 +1,206 @@ +{% extends "bootstrap_base.html" %} +{% load i18n humanize %} + +{% block page_title %}{% trans "Compliance Control Center" %}{% endblock %} + +{% block content %} +
        +

        + {% trans "Compliance Control Center" %} +

        + + {{ total_products }} {% trans "active product" %}{{ total_products|pluralize }} + + +
        + +
        +
        +
        +
        {% trans "Products with issues" %}
        +
        + {{ products_with_issues }} +
        +
        + {% if products_with_issues %} + {% trans "of" %} {{ total_products }} {% trans "active products" %} + {% else %} + {% trans "All" %} {{ total_products }} {% trans "products are compliant" %} + {% endif %} +
        +
        +
        +
        +
        +
        {% trans "License issues" %}
        +
        + {{ products_with_license_issues }} +
        +
        + {% if products_with_license_issues %} + {% trans "products with policy violations" %} + {% else %} + {% trans "All products within policy" %} + {% endif %} +
        +
        +
        +
        +
        +
        {% trans "Security issues" %}
        +
        + {{ products_with_critical_or_high }} +
        +
        + {% if products_with_critical_or_high %} + {% trans "products with critical/high vulnerabilities" %} + {% else %} + {% trans "No critical or high vulnerabilities" %} + {% endif %} +
        +
        +
        +
        +
        +
        {% trans "Total vulnerabilities" %}
        +
        + {{ total_vulnerabilities|intcomma }} +
        +
        + {% if total_critical %} + {{ total_critical }} {% trans "critical" %}{% if total_high %}, {{ total_high }} {% trans "high" %}{% endif %}{% if total_medium %}, {{ total_medium }} {% trans "medium" %}{% endif %}{% if total_low %}, {{ total_low }} {% trans "low" %}{% endif %} + {% elif total_vulnerabilities %} + {% trans "across all products" %} + {% else %} + {% trans "No known vulnerabilities" %} + {% endif %} +
        +
        +
        +
        + +
        + + + + + + + + + + + + {% for product in object_list %} + {% with product_url=product.get_absolute_url %} + + + + + + + + {% endwith %} + {% empty %} + + + + {% endfor %} + +
        {% trans "Product" %}{% trans "Packages" %}{% trans "License compliance" %}{% trans "Security compliance" %}{% trans "Vulnerabilities" %}
        + + {{ product }} + + + + {{ product.package_count|intcomma }} + + + {% if product.license_error_count %} + + {{ product.license_error_count }} {% trans "error" %}{{ product.license_error_count|pluralize }} + + {% endif %} + {% if product.license_warning_count %} + + {{ product.license_warning_count }} {% trans "warning" %}{{ product.license_warning_count|pluralize }} + + {% endif %} + {% if not product.license_error_count and not product.license_warning_count %} + {% trans "OK" %} + {% endif %} + + {% if product.max_risk_level == "critical" %} + {% trans "Critical" %} + {% elif product.max_risk_level == "high" %} + {% trans "High" %} + {% elif product.max_risk_level == "medium" %} + {% trans "Medium" %} + {% elif product.max_risk_level == "low" %} + {% trans "Low" %} + {% else %} + {% trans "OK" %} + {% endif %} + + {% if product.risk_threshold and product.vulnerability_count %} + + ≥ {{ product.risk_threshold }} + + {% endif %} + {% if product.critical_count %} + {{ product.critical_count }} {% trans "critical" %} + {% endif %} + {% if product.high_count %} + {{ product.high_count }} {% trans "high" %} + {% endif %} + {% if product.medium_count %} + {{ product.medium_count }} {% trans "medium" %} + {% endif %} + {% if product.low_count %} + {{ product.low_count }} {% trans "low" %} + {% endif %} + {% if not product.vulnerability_count %} + {% trans "None" %} + {% endif %} +
        + {% trans "No active products" %} +
        +
        + + {% if is_paginated %} +
        + {% include 'pagination/object_list_pagination.html' %} +
        + {% endif %} +{% endblock %} \ No newline at end of file diff --git a/product_portfolio/templates/product_portfolio/includes/scan_all_packages_modal.html b/product_portfolio/templates/product_portfolio/includes/scan_all_packages_modal.html deleted file mode 100644 index 4f06c9f5..00000000 --- a/product_portfolio/templates/product_portfolio/includes/scan_all_packages_modal.html +++ /dev/null @@ -1,24 +0,0 @@ - \ No newline at end of file diff --git a/product_portfolio/templates/product_portfolio/includes/edit_productpackage_modal.html b/product_portfolio/templates/product_portfolio/modals/edit_productpackage_modal.html similarity index 100% rename from product_portfolio/templates/product_portfolio/includes/edit_productpackage_modal.html rename to product_portfolio/templates/product_portfolio/modals/edit_productpackage_modal.html diff --git a/product_portfolio/templates/product_portfolio/includes/pull_project_data_modal.html b/product_portfolio/templates/product_portfolio/modals/pull_project_data_modal.html similarity index 100% rename from product_portfolio/templates/product_portfolio/includes/pull_project_data_modal.html rename to product_portfolio/templates/product_portfolio/modals/pull_project_data_modal.html diff --git a/product_portfolio/templates/product_portfolio/modals/scan_all_packages_modal.html b/product_portfolio/templates/product_portfolio/modals/scan_all_packages_modal.html new file mode 100644 index 00000000..6262fe46 --- /dev/null +++ b/product_portfolio/templates/product_portfolio/modals/scan_all_packages_modal.html @@ -0,0 +1,35 @@ +{% load crispy_forms_tags %} + \ No newline at end of file diff --git a/product_portfolio/templates/product_portfolio/modals/scan_delete_htmx_modal.html b/product_portfolio/templates/product_portfolio/modals/scan_delete_htmx_modal.html new file mode 100644 index 00000000..0bf67055 --- /dev/null +++ b/product_portfolio/templates/product_portfolio/modals/scan_delete_htmx_modal.html @@ -0,0 +1,36 @@ +{% load i18n %} + \ No newline at end of file diff --git a/product_portfolio/templates/product_portfolio/includes/scancode_project_status_modal.html b/product_portfolio/templates/product_portfolio/modals/scancode_project_status_modal.html similarity index 100% rename from product_portfolio/templates/product_portfolio/includes/scancode_project_status_modal.html rename to product_portfolio/templates/product_portfolio/modals/scancode_project_status_modal.html diff --git a/product_portfolio/templates/product_portfolio/product_details.html b/product_portfolio/templates/product_portfolio/product_details.html index 82b78da5..6f4f11cd 100644 --- a/product_portfolio/templates/product_portfolio/product_details.html +++ b/product_portfolio/templates/product_portfolio/product_details.html @@ -4,7 +4,13 @@ {% block pager-toolbar %} {% if is_user_dataspace %} - {% if has_edit_productcomponent or has_edit_productpackage or has_change_codebaseresource_permission or has_add_productcomponent %} + {% if product.is_locked %} + + + + {% elif has_edit_productcomponent or has_edit_productpackage or has_change_codebaseresource_permission or has_add_productcomponent %} @@ -109,29 +131,43 @@ {% block content %} {{ block.super }} {% if has_scan_all_packages %} - {% include 'product_portfolio/includes/scan_all_packages_modal.html' %} + {% include 'product_portfolio/modals/scan_all_packages_modal.html' %} {% endif %} {% if display_scan_features %} - {% include 'component_catalog/includes/scan_package_modal.html' %} + {% include 'component_catalog/includes/scan_package_modal.html' with is_xhr=True only %} + {% include 'product_portfolio/modals/scan_delete_htmx_modal.html' %} {% endif %} {% if pull_project_data_form %} - {% include 'product_portfolio/includes/pull_project_data_modal.html' %} + {% include 'product_portfolio/modals/pull_project_data_modal.html' %} {% endif %} {% if has_edit_productpackage or has_edit_productcomponent %} - {% include 'product_portfolio/includes/edit_productpackage_modal.html' %} + {% include 'product_portfolio/modals/edit_productpackage_modal.html' %} {% endif %} {% if tabsets.Imports %} - {% include 'product_portfolio/includes/scancode_project_status_modal.html' %} + {% include 'product_portfolio/modals/scancode_project_status_modal.html' %} {% endif %} {% if request.user.dataspace.enable_vulnerablecodedb_access and product.vulnerability_count %} {% include 'product_portfolio/modals/vulnerability_analysis_modal.html' %} {% endif %} {% endblock %} +{% block messages-alert %} + {% if product.is_locked %} +
        + +
        + {% endif %} + {{ block.super }} +{% endblock %} + {% block extrastyle %} {{ block.super }} {% if has_edit_productpackage or has_edit_productcomponent %} - + {% endif %} {% endblock %} @@ -148,20 +184,28 @@ {% if display_scan_features %} {% endif %} {% if has_scan_all_packages %} \ No newline at end of file diff --git a/product_portfolio/tests/__init__.py b/product_portfolio/tests/__init__.py index 039b43a5..0dbf07f0 100644 --- a/product_portfolio/tests/__init__.py +++ b/product_portfolio/tests/__init__.py @@ -18,6 +18,7 @@ from product_portfolio.models import ProductDependency from product_portfolio.models import ProductItemPurpose from product_portfolio.models import ProductPackage +from product_portfolio.models import ProductStatus def make_product(dataspace, inventory=None, **data): @@ -71,9 +72,24 @@ def make_product_component(product, component=None): def make_product_item_purpose(dataspace, **data): + if "label" not in data: + data["label"] = make_string(10) + if "text" not in data: + data["text"] = make_string(10) + return ProductItemPurpose.objects.create( - label=make_string(10), - text=make_string(10), + dataspace=dataspace, + **data, + ) + + +def make_product_status(dataspace, **data): + if "label" not in data: + data["label"] = make_string(10) + if "text" not in data: + data["text"] = make_string(10) + + return ProductStatus.objects.create( dataspace=dataspace, **data, ) diff --git a/product_portfolio/tests/test_api.py b/product_portfolio/tests/test_api.py index b5b27fcd..88b50bcd 100644 --- a/product_portfolio/tests/test_api.py +++ b/product_portfolio/tests/test_api.py @@ -352,6 +352,41 @@ def test_api_product_endpoint_update_permissions(self): product1 = Product.unsecured_objects.get(pk=self.product1.pk) self.assertEqual("Updated Name", product1.name) + def test_api_product_endpoint_imports_action(self): + url = reverse("api_v2:product-imports", args=[self.product1.uuid]) + + self.client.login(username=self.base_user.username, password="secret") + response = self.client.get(url) + self.assertEqual(status.HTTP_404_NOT_FOUND, response.status_code) + + # Required permissions + add_perm(self.base_user, "add_product") + assign_perm("view_product", self.base_user, self.product1) + + response = self.client.get(url) + self.assertEqual(status.HTTP_200_OK, response.status_code) + self.assertEqual(0, self.product1.scancodeprojects.count()) + self.assertEqual([], response.data) + + ScanCodeProject.objects.create( + product=self.product1, + dataspace=self.product1.dataspace, + type=ScanCodeProject.ProjectType.LOAD_SBOMS, + status=ScanCodeProject.Status.SUCCESS, + input_file=ContentFile("Data", name="data.json"), + ) + + response = self.client.get(url) + self.assertEqual(status.HTTP_200_OK, response.status_code) + self.assertEqual(1, self.product1.scancodeprojects.count()) + self.assertEqual(1, len(response.data)) + entry = response.data[0] + self.assertEqual(ScanCodeProject.ProjectType.LOAD_SBOMS, entry["type"]) + self.assertEqual("data.json", entry["input_filename"]) + self.assertEqual(ScanCodeProject.Status.SUCCESS, entry["status"]) + self.assertEqual([], entry["import_log"]) + self.assertEqual({}, entry["results"]) + def test_api_product_endpoint_load_sboms_action(self): url = reverse("api_v2:product-load-sboms", args=[self.product1.uuid]) @@ -372,6 +407,7 @@ def test_api_product_endpoint_load_sboms_action(self): data = { "input_file": ContentFile("{}", name="sbom.json"), + "infer_download_urls": True, "update_existing_packages": False, "scan_all_packages": False, } @@ -401,6 +437,7 @@ def test_api_product_endpoint_import_manifests_action(self): data = { "input_file": ContentFile("Content", name="requirements.txt"), + "infer_download_urls": True, "update_existing_packages": False, "scan_all_packages": False, } @@ -439,12 +476,13 @@ def test_api_product_endpoint_import_from_scan_action(self): self.assertEqual(expected, response.data) scan_input_location = self.testfiles_path / "import_from_scan.json" - data = { - "upload_file": scan_input_location.open(), - "create_codebase_resources": True, - "stop_on_error": False, - } - response = self.client.post(url, data) + with scan_input_location.open() as upload_file: + data = { + "upload_file": upload_file, + "create_codebase_resources": True, + "stop_on_error": False, + } + response = self.client.post(url, data) self.assertEqual(status.HTTP_200_OK, response.status_code) expected = { "status": "Imported from Scan: 1 Packages, 1 Product Packages, 3 Codebase Resources" @@ -535,7 +573,7 @@ def test_api_product_endpoint_cyclonedx_sbom_action(self): response = self.client.get(url) self.assertEqual(status.HTTP_200_OK, response.status_code) - expected = 'attachment; filename="dejacode_nexb_product_p1.cdx.json"' + expected = 'attachment; filename="dejacode_nexb_p1.cdx.json"' self.assertEqual(expected, response["Content-Disposition"]) self.assertEqual("application/json", response["Content-Type"]) self.assertIn('"specVersion": "1.6"', str(response.getvalue())) diff --git a/product_portfolio/tests/test_filters.py b/product_portfolio/tests/test_filters.py new file mode 100644 index 00000000..ce167547 --- /dev/null +++ b/product_portfolio/tests/test_filters.py @@ -0,0 +1,83 @@ +# +# Copyright (c) nexB Inc. and others. All rights reserved. +# DejaCode is a trademark of nexB Inc. +# SPDX-License-Identifier: AGPL-3.0-only +# See https://github.com/aboutcode-org/dejacode for support or download. +# See https://aboutcode.org for more information about AboutCode FOSS projects. +# + +from django.test import TestCase + +from component_catalog.tests import make_package +from dje.models import Dataspace +from license_library.models import License +from organization.models import Owner +from product_portfolio.filters import ProductPackageFilterSet +from product_portfolio.models import ProductPackage +from product_portfolio.tests import make_product + + +class ProductPackageFilterSetTestCase(TestCase): + def setUp(self): + self.dataspace = Dataspace.objects.create(name="Reference") + self.owner = Owner.objects.create(name="Owner1", dataspace=self.dataspace) + self.license1 = License.objects.create( + key="mit", + name="MIT", + short_name="MIT", + owner=self.owner, + dataspace=self.dataspace, + ) + self.license2 = License.objects.create( + key="apache-2.0", + name="Apache 2.0", + short_name="Apache 2.0", + owner=self.owner, + dataspace=self.dataspace, + ) + + self.product = make_product(self.dataspace) + self.package1 = make_package(self.dataspace) + self.package2 = make_package(self.dataspace) + self.package3 = make_package(self.dataspace) + + self.pp1 = ProductPackage.objects.create( + product=self.product, + package=self.package1, + license_expression=self.license1.key, + dataspace=self.dataspace, + ) + self.pp2 = ProductPackage.objects.create( + product=self.product, + package=self.package2, + license_expression=self.license2.key, + dataspace=self.dataspace, + ) + self.pp3 = ProductPackage.objects.create( + product=self.product, + package=self.package3, + license_expression="", + dataspace=self.dataspace, + ) + + def test_product_package_filterset_licenses(self): + data = {"licenses": [self.license1.key]} + filterset = ProductPackageFilterSet(dataspace=self.dataspace, data=data) + self.assertQuerySetEqual(filterset.qs, [self.pp1]) + + data = {"licenses": [self.license2.key]} + filterset = ProductPackageFilterSet(dataspace=self.dataspace, data=data) + self.assertQuerySetEqual(filterset.qs, [self.pp2]) + + data = {"licenses": [self.license1.key, self.license2.key]} + filterset = ProductPackageFilterSet(dataspace=self.dataspace, data=data) + self.assertQuerySetEqual(filterset.qs, [self.pp1, self.pp2], ordered=False) + + def test_product_package_filterset_has_licenses(self): + data = {"has_licenses": "yes"} + filterset = ProductPackageFilterSet(dataspace=self.dataspace, data=data) + self.assertQuerySetEqual(filterset.qs, [self.pp1, self.pp2], ordered=False) + + data = {"has_licenses": "no"} + filterset = ProductPackageFilterSet(dataspace=self.dataspace, data=data) + self.assertQuerySetEqual(filterset.qs, [self.pp3]) diff --git a/product_portfolio/tests/test_importers.py b/product_portfolio/tests/test_importers.py index e587f198..cdfe83b5 100644 --- a/product_portfolio/tests/test_importers.py +++ b/product_portfolio/tests/test_importers.py @@ -19,6 +19,7 @@ from component_catalog.models import Component from component_catalog.models import Package +from component_catalog.tests import make_package from dje.models import Dataspace from dje.tests import create_admin from dje.tests import create_superuser @@ -973,13 +974,13 @@ def test_product_portfolio_product_import_from_scan_view_empty_packages(self): def test_product_portfolio_import_packages_from_scancodeio_importer( self, mock_fetch_packages, mock_fetch_dependencies ): - purl = "pkg:maven/org.apache.activemq/activemq-camel@5.11.0" + purl = "pkg:maven/abc/abc@1.0" mock_fetch_packages.return_value = [ { "type": "maven", - "namespace": "org.apache.activemq", - "name": "activemq-camel", - "version": "5.11.0", + "namespace": "abc", + "name": "abc", + "version": "1.0", "primary_language": "Java", "purl": purl, "declared_license_expression": "bsd-new", @@ -1010,12 +1011,15 @@ def test_product_portfolio_import_packages_from_scancodeio_importer( user=self.super_user, project_uuid=uuid.uuid4(), product=self.product1, + infer_download_urls=True, ) created, existing, errors = importer.save() created_package_package_url = created.get("package")[0] created_package = self.product1.packages.get() self.assertEqual("bsd-new", created_package.license_expression) self.assertEqual(created_package.package_url, created_package_package_url) + inferred_download_url = "https://repo.maven.apache.org/maven2/abc/abc/1.0/abc-1.0.jar" + self.assertEqual(inferred_download_url, created_package.download_url) self.assertEqual({}, existing) self.assertEqual({}, errors) @@ -1055,3 +1059,250 @@ def test_product_portfolio_import_packages_from_scancodeio_importer( ) importer.save() mock_fetch.assert_called() + + @mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.fetch_project_dependencies") + @mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.fetch_project_packages") + def test_product_portfolio_import_packages_from_scio_importer_multiple_package_objs( + self, mock_fetch_packages, mock_fetch_dependencies + ): + purl = "pkg:maven/org.apache.activemq/activemq-camel@5.11.0" + filename = "activemq-camel.zip" + + package_data = { + "type": "maven", + "namespace": "org.apache.activemq", + "name": "activemq-camel", + "version": "5.11.0", + "primary_language": "Java", + "purl": purl, + "declared_license_expression": "bsd-new", + } + mock_fetch_packages.return_value = [package_data] + mock_fetch_dependencies.return_value = [] + + package1 = make_package(self.dataspace, package_url=purl) + package2 = make_package(self.dataspace, package_url=purl, filename=filename) + + importer = ImportPackageFromScanCodeIO( + user=self.super_user, + project_uuid=uuid.uuid4(), + product=self.product1, + ) + created, existing, errors = importer.save() + self.assertEqual({}, created) + self.assertEqual(purl, existing["package"][0]) + self.assertEqual({}, errors) + # The package without the filename or download_url is used + self.assertEqual(package1, self.product1.packages.get()) + + self.product1.productpackages.all().delete() + package_data["filename"] = filename + importer = ImportPackageFromScanCodeIO( + user=self.super_user, + project_uuid=uuid.uuid4(), + product=self.product1, + ) + created, existing, errors = importer.save() + self.assertEqual({}, created) + self.assertEqual(purl, existing["package"][0]) + self.assertEqual({}, errors) + # The package with the filename is used + self.assertEqual(package2, self.product1.packages.get()) + + self.product1.productpackages.all().delete() + package_data["filename"] = "DO_NOT_EXISTS" + importer = ImportPackageFromScanCodeIO( + user=self.super_user, + project_uuid=uuid.uuid4(), + product=self.product1, + ) + created, existing, errors = importer.save() + # New package is created. + self.assertEqual( + {"package": ["pkg:maven/org.apache.activemq/activemq-camel@5.11.0"]}, created + ) + self.assertEqual({}, existing) + self.assertEqual({}, errors) + + @mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.fetch_project_dependencies") + @mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.fetch_project_packages") + def test_product_portfolio_import_packages_from_scio_importer_look_for_existing_package( + self, mock_fetch_packages, mock_fetch_dependencies + ): + purl = "pkg:maven/org.apache.activemq/activemq-camel@5.11.0" + filename = "activemq-camel.zip" + download_url = "https://download.url/activemq-camel.zip" + package1 = make_package(self.dataspace, package_url=purl) + package2 = make_package(self.dataspace, package_url=purl, filename=filename) + + package_data = { + "type": "maven", + "namespace": "org.apache.activemq", + "name": "activemq-camel", + "version": "5.11.0", + "purl": purl, + } + mock_fetch_packages.return_value = [package_data] + mock_fetch_dependencies.return_value = [] + + importer = ImportPackageFromScanCodeIO( + user=self.super_user, + project_uuid=uuid.uuid4(), + product=self.product1, + ) + + # Check if the Package already exists in the local Dataspace + # Using exact match first: purl + download_url + filename + package = importer.look_for_existing_package(package_data) + self.assertEqual(package1, package) + + # 2 packages are matched, cannot define the one that should be used + package1.update(download_url=download_url) + package = importer.look_for_existing_package(package_data) + self.assertIsNone(package) + + # If the package data does not include a download_url value: + # Attempt to find an existing package using purl-only match. + package2.delete() + package = importer.look_for_existing_package(package_data) + self.assertEqual(package1, package) + + @mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.fetch_project_dependencies") + @mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.fetch_project_packages") + def test_product_portfolio_import_packages_from_scio_importer_duplicate_dependency( + self, mock_fetch_packages, mock_fetch_dependencies + ): + for_package_purl = "pkg:pypi/package@1.0" + for_package_uid = "6eb9d787-2f2a-40f3-8815-fbf8f6c373de" + resolved_to_package_purl = "pkg:pypi/dep@0.5" + resolved_to_package_uid = "895de5cb-2d40-4532-ae46-56104cd6a1bf" + mock_fetch_packages.return_value = [ + { + "type": "pypi", + "name": "package", + "version": "1.0", + "purl": for_package_purl, + "package_uid": for_package_uid, + }, + { + "type": "pypi", + "name": "dep", + "version": "0.5", + "purl": resolved_to_package_purl, + "package_uid": resolved_to_package_uid, + }, + ] + + dependency_uid = "12a4113b-99d2-455a-a96d-468ca29861d6" + mock_fetch_dependencies.return_value = [ + { + "dependency_uid": dependency_uid, + "for_package_uid": for_package_uid, + "resolved_to_package_uid": resolved_to_package_uid, + } + ] + + importer = ImportPackageFromScanCodeIO( + user=self.super_user, + project_uuid=uuid.uuid4(), + product=self.product1, + ) + created, existing, errors = importer.save() + expected = { + "package": ["pkg:pypi/package@1.0", "pkg:pypi/dep@0.5"], + "dependency": ["12a4113b-99d2-455a-a96d-468ca29861d6"], + } + self.assertEqual(expected, created) + self.assertEqual({}, existing) + self.assertEqual({}, errors) + self.assertEqual(2, self.product1.packages.count()) + self.assertEqual(1, self.product1.dependencies.count()) + + # Re-run the importer and make sure no duplicate entries are created + importer = ImportPackageFromScanCodeIO( + user=self.super_user, + project_uuid=uuid.uuid4(), + product=self.product1, + ) + created, existing, errors = importer.save() + self.assertEqual({}, created) + self.assertEqual(expected, existing) + self.assertEqual({}, errors) + self.assertEqual(2, self.product1.packages.count()) + self.assertEqual(1, self.product1.dependencies.count()) + + # Change the dependency_uid to make sure the match happen on the FKs as well. + dependency_uid = "a46c682f-51a7-44a4-a00f-ccfc826befc6" + mock_fetch_dependencies.return_value = [ + { + "dependency_uid": dependency_uid, + "for_package_uid": for_package_uid, + "resolved_to_package_uid": resolved_to_package_uid, + } + ] + importer = ImportPackageFromScanCodeIO( + user=self.super_user, + project_uuid=uuid.uuid4(), + product=self.product1, + ) + created, existing, errors = importer.save() + self.assertEqual({}, created) + expected = { + "package": ["pkg:pypi/package@1.0", "pkg:pypi/dep@0.5"], + "dependency": ["a46c682f-51a7-44a4-a00f-ccfc826befc6"], + } + self.assertEqual(expected, existing) + self.assertEqual({}, errors) + self.assertEqual(2, self.product1.packages.count()) + self.assertEqual(1, self.product1.dependencies.count()) + + @mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.fetch_project_dependencies") + @mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.fetch_project_packages") + def test_product_portfolio_import_packages_from_scio_importer_vex( + self, mock_fetch_packages, mock_fetch_dependencies + ): + vulnerability_data = { + "id": "ID-0001", # In CycloneDX the field name is "id" + "summary": "complexity bugs may lead to a denial of service", + "cdx_vulnerability_data": { + "affects": [{"ref": "pkg:maven/abc/abc@1.0"}], + "bom-ref": "BomRef.1", + "description": "complexity bugs may lead to a denial of service", + "analysis": { + "detail": "AAAA", + "justification": "code_not_present", + "response": ["can_not_fix", "update"], + "state": "resolved", + }, + }, + } + mock_fetch_packages.return_value = [ + { + "purl": "pkg:maven/abc/abc@1.0", + "type": "maven", + "namespace": "abc", + "name": "abc", + "version": "1.0", + "affected_by_vulnerabilities": [vulnerability_data], + } + ] + + importer = ImportPackageFromScanCodeIO( + user=self.super_user, + project_uuid=uuid.uuid4(), + product=self.product1, + ) + created, existing, errors = importer.save() + created_package = self.product1.packages.get() + vulnerability = created_package.affected_by_vulnerabilities.get() + self.assertEqual(vulnerability_data["id"], vulnerability.vulnerability_id) + self.assertEqual(vulnerability_data["summary"], vulnerability.summary) + + analysis = vulnerability.vulnerability_analyses.get() + self.assertEqual(vulnerability, analysis.vulnerability) + self.assertEqual(self.product1, analysis.product) + self.assertEqual(created_package, analysis.package) + self.assertEqual("resolved", analysis.state) + self.assertEqual("code_not_present", analysis.justification) + self.assertEqual("AAAA", analysis.detail) + self.assertEqual(["can_not_fix", "update"], analysis.responses) diff --git a/product_portfolio/tests/test_models.py b/product_portfolio/tests/test_models.py index 40f6766a..f5abe9d2 100644 --- a/product_portfolio/tests/test_models.py +++ b/product_portfolio/tests/test_models.py @@ -37,12 +37,12 @@ from product_portfolio.models import ProductPackage from product_portfolio.models import ProductRelationStatus from product_portfolio.models import ProductSecuredManager -from product_portfolio.models import ProductStatus from product_portfolio.models import ScanCodeProject from product_portfolio.tests import make_product from product_portfolio.tests import make_product_dependency from product_portfolio.tests import make_product_item_purpose from product_portfolio.tests import make_product_package +from product_portfolio.tests import make_product_status from vulnerabilities.tests import make_vulnerability from workflow.models import RequestTemplate @@ -69,19 +69,15 @@ def setUp(self): ) def test_product_model_default_status_on_product_addition(self): - status1 = ProductStatus.objects.create( - label="S1", text="Status1", default_on_addition=True, dataspace=self.dataspace - ) - status2 = ProductStatus.objects.create(label="S2", text="Status2", dataspace=self.dataspace) + status1 = make_product_status(default_on_addition=True, dataspace=self.dataspace) + status2 = make_product_status(dataspace=self.dataspace) # No status given at creation time, the default is set - p1 = Product.objects.create(name="P1", dataspace=self.dataspace) + p1 = make_product(dataspace=self.dataspace, name="P1") self.assertEqual(status1, p1.configuration_status) # A status is given at creation time, no default is set - p2 = Product.objects.create( - name="P2", configuration_status=status2, dataspace=self.dataspace - ) + p2 = make_product(dataspace=self.dataspace, name="P2", configuration_status=status2) self.assertEqual(status2, p2.configuration_status) def test_product_model_get_attribution_url(self): @@ -105,6 +101,19 @@ def test_product_model_secured_manager(self): self.assertEqual(1, Product.objects.get_queryset(self.super_user).count()) self.assertIn(self.product1, Product.objects.get_queryset(self.super_user)) + def test_product_model_secured_manager_get_queryset_exclude_locked(self): + qs = Product.objects.get_queryset(self.super_user) + self.assertIn(self.product1, qs) + qs = Product.objects.get_queryset(self.super_user, exclude_locked=True) + self.assertIn(self.product1, qs) + + locked_status = make_product_status(self.dataspace, is_locked=True) + self.product1.update(configuration_status=locked_status) + qs = Product.objects.get_queryset(self.super_user) + self.assertIn(self.product1, qs) + qs = Product.objects.get_queryset(self.super_user, exclude_locked=True) + self.assertNotIn(self.product1, qs) + def test_product_model_is_active(self): qs = Product.objects.get_queryset(self.super_user) self.assertIn(self.product1, qs) @@ -146,8 +155,11 @@ def test_product_model_all_packages(self): def test_product_model_get_vulnerable_packages(self): self.assertEqual(0, self.product1.get_vulnerable_packages().count()) - package1 = make_package(self.dataspace, is_vulnerable=True, risk_score=5.0) + package1 = make_package(self.dataspace) + vulnerability1 = make_vulnerability(self.dataspace, risk_score=5.0) + package1.add_affected_by(vulnerability1) make_product_package(self.product1, package1) + self.assertEqual(1, self.product1.get_vulnerable_packages().count()) self.assertEqual(0, self.product1.get_vulnerable_packages(risk_threshold=6.0).count()) self.assertEqual(1, self.product1.get_vulnerable_packages(risk_threshold=4.0).count()) @@ -401,6 +413,21 @@ def test_product_model_assign_object_replace_version_package(self): expected_message = 'Updated package "pkg:deb/debian/curl@1.0" to "pkg:deb/debian/curl@2.0"' self.assertEqual(expected_message, history_entries.latest("action_time").change_message) + def test_product_model_assign_object_replace_version_package_update_vulnerability_scores(self): + self.assertEqual(0, self.product1.get_vulnerable_productpackages().count()) + package1 = make_package(self.dataspace, name="a", version="1.0", is_vulnerable=True) + p1_p1 = make_product_package(self.product1, package1) + p1_p1.raw_update(weighted_risk_score=5.0) + self.assertTrue(self.product1.productpackages.vulnerable().exists()) + + package2 = make_package(self.dataspace, name="a", version="2.0") + status, p1_p2 = self.product1.assign_object(package2, self.super_user, replace_version=True) + self.assertEqual("updated", status) + + p1_p2.refresh_from_db() + self.assertIsNone(p1_p2.weighted_risk_score) + self.assertFalse(self.product1.productpackages.vulnerable().exists()) + def test_product_model_find_assigned_other_versions_component(self): component1 = Component.objects.create(name="c", version="1.0", dataspace=self.dataspace) component2 = Component.objects.create(name="c", version="2.0", dataspace=self.dataspace) @@ -490,14 +517,12 @@ def test_product_model_actions_on_status_change(self): dataspace=self.super_user.dataspace, content_type=product_ct, ) - - status1 = ProductStatus.objects.create( + status1 = make_product_status( + self.dataspace, label="status1", text="status1", request_to_generate=request_template1, - dataspace=self.dataspace, ) - product.configuration_status = status1 product.last_modified_by = self.super_user self.assertTrue(product.has_changed("configuration_status_id")) @@ -509,6 +534,58 @@ def test_product_model_actions_on_status_change(self): product.refresh_from_db() self.assertEqual(1, product.request_count) + def test_product_model_is_locked_property(self): + product = make_product(self.dataspace) + self.assertFalse(product.is_locked) + + status1 = make_product_status(self.dataspace, is_locked=False) + product.update(configuration_status=status1) + product = Product.unsecured_objects.get(pk=product.pk) + self.assertFalse(product.is_locked) + + status1.update(is_locked=True) + product = Product.unsecured_objects.get(pk=product.pk) + self.assertTrue(product.is_locked) + + def test_product_model_improve_packages_from_purl(self): + product = make_product(self.dataspace) + package1 = make_package(self.dataspace, package_url="pkg:nuget/Azure.Core@1.45.0") + make_product_package(product, package=package1) + + product.refresh_from_db() + updated_packages = product.improve_packages_from_purl() + self.assertEqual([package1], updated_packages) + + package1.refresh_from_db() + expected_download_url = "https://www.nuget.org/api/v2/package/Azure.Core/1.45.0" + self.assertEqual(expected_download_url, package1.download_url) + + @mock.patch("dje.tasks.scancodeio_submit_scan.delay") + def test_product_model_scan_all_packages_task(self, mock_scancodeio_submit_scan): + product = make_product(self.dataspace) + package1 = make_package(self.dataspace, package_url="pkg:nuget/Azure.Core@1.45.0") + make_product_package(product, package=package1) + + mock_scancodeio_submit_scan.return_value = None + product.scan_all_packages_task(user=self.super_user) + mock_scancodeio_submit_scan.assert_called_with( + uris=[], + user_uuid=self.super_user.uuid, + dataspace_uuid=self.super_user.dataspace.uuid, + ) + package1.refresh_from_db() + self.assertEqual("", package1.download_url) + + expected_download_url = "https://www.nuget.org/api/v2/package/Azure.Core/1.45.0" + product.scan_all_packages_task(user=self.super_user, infer_download_urls=True) + mock_scancodeio_submit_scan.assert_called_with( + uris=[expected_download_url], + user_uuid=self.super_user.uuid, + dataspace_uuid=self.super_user.dataspace.uuid, + ) + package1.refresh_from_db() + self.assertEqual(expected_download_url, package1.download_url) + @mock.patch("component_catalog.models.Package.update_from_purldb") def test_product_model_improve_packages_from_purldb(self, mock_update_from_purldb): mock_update_from_purldb.return_value = 1 @@ -526,6 +603,27 @@ def test_product_model_improve_packages_from_purldb(self, mock_update_from_purld pp1.refresh_from_db() self.assertEqual("apache-2.0", pp1.license_expression) + def test_product_model_affected_by_vulnerabilities(self): + vulnerability1 = make_vulnerability(self.dataspace, risk_score=1.0) + vulnerability2 = make_vulnerability(self.dataspace, risk_score=10.0) + vulnerability3 = make_vulnerability(self.dataspace, risk_score=5.0) + + vulnerability1.add_affected(self.product1) + affected_by = self.product1.affected_by_vulnerabilities.all() + self.assertQuerySetEqual([vulnerability1], affected_by) + self.product1.refresh_from_db() + self.assertEqual(1.0, self.product1.risk_score) + + vulnerability2.add_affected(self.product1) + affected_by = self.product1.affected_by_vulnerabilities.order_by("id") + self.assertQuerySetEqual([vulnerability1, vulnerability2], affected_by) + self.product1.refresh_from_db() + self.assertEqual(10.0, self.product1.risk_score) + + vulnerability3.add_affected(self.product1) + self.product1.refresh_from_db() + self.assertEqual(10.0, self.product1.risk_score) + def test_product_model_get_vulnerability_qs(self): package1 = make_package(self.dataspace) package2 = make_package(self.dataspace) @@ -760,6 +858,19 @@ def test_productcomponent_model_is_custom_component(self): pc1.save() self.assertFalse(pc1.is_custom_component) + def test_productpackage_model_update_license_unknown(self): + package1 = make_package(self.dataspace, declared_license_expression="mit") + pp1 = make_product_package(self.product1, package=package1) + + pp1.update_license_unknown() + pp1.refresh_from_db() + self.assertEqual("", pp1.license_expression) + + pp1.update(license_expression="unknown") + pp1.update_license_unknown() + pp1.refresh_from_db() + self.assertEqual("mit", pp1.license_expression) + def test_product_relationship_queryset_vulnerable(self): pp1 = make_product_package(self.product1) product_package_qs = ProductPackage.objects.vulnerable() @@ -813,6 +924,70 @@ def test_product_relationship_queryset_update_weighted_risk_score(self): pp1.refresh_from_db() self.assertIsNone(pp1.weighted_risk_score) + def test_productpackage_queryset_exclude_locked_products(self): + active_product = make_product(self.dataspace) + pp1 = make_product_package(active_product) + inactive_product = make_product(self.dataspace, is_active=False) + make_product_package(inactive_product) + + qs = ProductPackage.objects.exclude_locked_products() + self.assertQuerySetEqual(qs, [pp1]) + + locked_status = make_product_status(self.dataspace, is_locked=True) + active_product.update(configuration_status=locked_status) + qs = ProductPackage.objects.exclude_locked_products() + self.assertQuerySetEqual(qs, []) + + def test_productpackage_queryset_license_unknown(self): + package1 = make_package(self.dataspace, declared_license_expression="unknown") + package2 = make_package(self.dataspace, declared_license_expression="mit") + pp1 = make_product_package(self.product1, package=package1) + pp2 = make_product_package(self.product1, package=package2) + + qs = ProductPackage.objects.license_unknown() + self.assertQuerySetEqual([], qs) + + pp1.update(license_expression="unknown") + pp2.update(license_expression="mit") + qs = ProductPackage.objects.license_unknown() + self.assertQuerySetEqual(qs, [pp1]) + + def test_productpackage_queryset_update_license_unknown(self): + package1 = make_package(self.dataspace, declared_license_expression="mit") + package2 = make_package(self.dataspace, declared_license_expression="mit") + pp1 = make_product_package(self.product1, package=package1) + pp2 = make_product_package(self.product1, package=package2) + + ProductPackage.objects.update_license_unknown() + pp1.refresh_from_db() + pp2.refresh_from_db() + self.assertEqual("", pp1.license_expression) + self.assertEqual("", pp2.license_expression) + + pp1.update(license_expression="unknown") + ProductPackage.objects.update_license_unknown() + pp1.refresh_from_db() + pp2.refresh_from_db() + self.assertEqual("mit", pp1.license_expression) + self.assertEqual("", pp2.license_expression) + + def test_productpackage_queryset_update_license_unknown_exclude_locked_products(self): + locked_status = make_product_status(self.dataspace, is_locked=True) + self.product1.update(configuration_status=locked_status) + + package1 = make_package(self.dataspace, declared_license_expression="mit") + pp1 = make_product_package(self.product1, package=package1, license_expression="unknown") + + ProductPackage.objects.update_license_unknown() + pp1.refresh_from_db() + # Product is locked + self.assertEqual("unknown", pp1.license_expression) + + self.product1.update(configuration_status=None) + ProductPackage.objects.update_license_unknown() + pp1.refresh_from_db() + self.assertEqual("mit", pp1.license_expression) + def test_productrelation_model_compute_weighted_risk_score(self): purpose1 = make_product_item_purpose(self.dataspace) diff --git a/product_portfolio/tests/test_tasks.py b/product_portfolio/tests/test_tasks.py new file mode 100644 index 00000000..896b69a7 --- /dev/null +++ b/product_portfolio/tests/test_tasks.py @@ -0,0 +1,215 @@ +# +# Copyright (c) nexB Inc. and others. All rights reserved. +# DejaCode is a trademark of nexB Inc. +# SPDX-License-Identifier: AGPL-3.0-only +# See https://github.com/aboutcode-org/dejacode for support or download. +# See https://aboutcode.org for more information about AboutCode FOSS projects. +# + +import uuid +from unittest import mock + +from django.core.files.base import ContentFile +from django.db import IntegrityError +from django.test import TestCase + +from guardian.shortcuts import assign_perm +from notifications.models import Notification + +from dje.models import Dataspace +from dje.tests import MaxQueryMixin +from dje.tests import create_superuser +from dje.tests import create_user +from product_portfolio.models import ScanCodeProject +from product_portfolio.tasks import improve_packages_from_purldb_task +from product_portfolio.tasks import logger as tasks_logger +from product_portfolio.tasks import pull_project_data_from_scancodeio_task +from product_portfolio.tasks import scancodeio_submit_project_task +from product_portfolio.tests import make_product + + +class ProductPortfolioTasksTestCase(MaxQueryMixin, TestCase): + def setUp(self): + self.dataspace = Dataspace.objects.create(name="nexB") + self.super_user = create_superuser("nexb_user", self.dataspace) + self.basic_user = create_user("basic_user", self.dataspace) + self.product1 = make_product(self.dataspace) + + @mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.submit_project") + def test_scancodeio_submit_project_task(self, mock_submit_project): + scancodeproject = ScanCodeProject.objects.create( + product=self.product1, + dataspace=self.product1.dataspace, + type=ScanCodeProject.ProjectType.LOAD_SBOMS, + input_file=ContentFile("Data", name="data.json"), + ) + + mock_submit_project.return_value = None + scancodeio_submit_project_task( + scancodeproject_uuid=scancodeproject.uuid, + user_uuid=self.super_user.uuid, + pipeline_name="load_sboms", + ) + scancodeproject.refresh_from_db() + self.assertEqual("failure", scancodeproject.status) + self.assertIsNone(scancodeproject.project_uuid) + expected = ["- Error: File could not be submitted to ScanCode.io"] + self.assertEqual(expected, scancodeproject.import_log) + + # Reset the instance values + scancodeproject.status = "" + scancodeproject.import_log = [] + scancodeproject.save() + + project_uuid = uuid.uuid4() + mock_submit_project.return_value = {"uuid": project_uuid} + scancodeio_submit_project_task( + scancodeproject_uuid=scancodeproject.uuid, + user_uuid=self.super_user.uuid, + pipeline_name="load_sboms", + ) + + scancodeproject.refresh_from_db() + self.assertEqual("submitted", scancodeproject.status) + self.assertEqual(project_uuid, scancodeproject.project_uuid) + expected = ["- File submitted to ScanCode.io for inspection"] + self.assertEqual(expected, scancodeproject.import_log) + + @mock.patch("product_portfolio.models.ScanCodeProject.import_data_from_scancodeio") + def test_product_portfolio_pull_project_data_from_scancodeio_task(self, mock_import_data): + scancode_project = ScanCodeProject.objects.create( + product=self.product1, + dataspace=self.product1.dataspace, + type=ScanCodeProject.ProjectType.PULL_FROM_SCANCODEIO, + created_by=self.super_user, + ) + + mock_import_data.side_effect = Exception("Error") + pull_project_data_from_scancodeio_task(scancodeproject_uuid=scancode_project.uuid) + scancode_project.refresh_from_db() + self.assertEqual(ScanCodeProject.Status.FAILURE, scancode_project.status) + self.assertEqual(["Error"], scancode_project.import_log) + notif = Notification.objects.get() + self.assertTrue(notif.unread) + self.assertEqual(self.super_user, notif.actor) + self.assertEqual("Import packages from ScanCode.io", notif.verb) + self.assertEqual(self.product1, notif.action_object) + self.assertEqual(self.super_user, notif.recipient) + self.assertEqual("Import failed.", notif.description) + + Notification.objects.all().delete() + scancode_project.import_log = [] + scancode_project.status = ScanCodeProject.Status.SUBMITTED + scancode_project.save() + mock_import_data.side_effect = None + mock_import_data.return_value = ( + {"package": ["package1"]}, + {"package": ["package2"]}, + {"package": ["error1"]}, + ) + pull_project_data_from_scancodeio_task(scancodeproject_uuid=scancode_project.uuid) + scancode_project.refresh_from_db() + self.assertEqual(ScanCodeProject.Status.SUCCESS, scancode_project.status) + expected = [ + "- Imported 1 package.", + "- 1 package skipped: already available in the dataspace.", + "- 1 package error occurred during import.", + ] + self.assertEqual(expected, scancode_project.import_log) + + notif = Notification.objects.get() + self.assertTrue(notif.unread) + self.assertEqual(self.super_user, notif.actor) + self.assertEqual("Import packages from ScanCode.io", notif.verb) + self.assertEqual(self.product1, notif.action_object) + self.assertEqual(self.super_user, notif.recipient) + self.assertEqual("\n".join(scancode_project.import_log), notif.description) + + def test_product_portfolio_pull_project_data_from_scancodeio_task_can_start_import(self): + scancode_project = ScanCodeProject.objects.create( + product=self.product1, + dataspace=self.product1.dataspace, + type=ScanCodeProject.ProjectType.PULL_FROM_SCANCODEIO, + status=ScanCodeProject.Status.IMPORT_STARTED, + created_by=self.super_user, + ) + + with self.assertLogs(tasks_logger) as cm: + pull_project_data_from_scancodeio_task(scancodeproject_uuid=scancode_project.uuid) + + expected = [ + f"INFO:product_portfolio.tasks:Entering pull_project_data_from_scancodeio task with " + f"scancodeproject_uuid={scancode_project.uuid}", + "ERROR:product_portfolio.tasks:Cannot start import", + ] + self.assertEqual(expected, cm.output) + + @mock.patch("product_portfolio.models.Product.improve_packages_from_purldb") + def test_product_portfolio_improve_packages_from_purldb_task(self, mock_improve): + mock_improve.return_value = ["pkg1", "pkg2"] + + self.assertFalse(self.basic_user.has_perm("change_product", self.product1)) + with self.assertLogs(tasks_logger) as cm: + improve_packages_from_purldb_task(self.product1.uuid, self.basic_user.uuid) + + expected = ( + "ERROR:product_portfolio.tasks:[improve_packages_from_purldb]: " + f"Product uuid={self.product1.uuid} does not exists." + ) + self.assertIn(expected, cm.output) + + assign_perm("view_product", self.basic_user, self.product1) + self.assertFalse(self.basic_user.has_perm("change_product", self.product1)) + with self.assertLogs(tasks_logger) as cm: + improve_packages_from_purldb_task(self.product1.uuid, self.basic_user.uuid) + + expected = ( + "ERROR:product_portfolio.tasks:[improve_packages_from_purldb]: Permission denied." + ) + self.assertIn(expected, cm.output) + + self.assertTrue(self.super_user.has_perm("change_product", self.product1)) + with self.assertLogs(tasks_logger) as cm: + improve_packages_from_purldb_task(self.product1.uuid, self.super_user.uuid) + + mock_improve.assert_called_once() + expected = [ + "INFO:product_portfolio.tasks:Entering improve_packages_from_purldb task with " + f"product_uuid={self.product1.uuid} " + f"user_uuid={self.super_user.uuid}", + "INFO:product_portfolio.tasks:[improve_packages_from_purldb]: 2 updated from PurlDB.", + ] + self.assertEqual(expected, cm.output) + + import_project = self.product1.scancodeprojects.get() + self.assertEqual(import_project.type, ScanCodeProject.ProjectType.IMPROVE_FROM_PURLDB) + self.assertEqual(import_project.status, ScanCodeProject.Status.SUCCESS) + expected = ["Improved packages from PurlDB:", "pkg1, pkg2"] + self.assertEqual(expected, import_project.import_log) + + notification = Notification.objects.get() + self.assertEqual("Improved packages from PurlDB:", notification.verb) + self.assertEqual("pkg1, pkg2", notification.description) + self.assertEqual("dejacodeuser", notification.actor_content_type.model) + self.assertEqual(self.product1, notification.action_object) + + @mock.patch("product_portfolio.models.Product.improve_packages_from_purldb") + def test_product_portfolio_improve_packages_from_purldb_task_exception(self, mock_improve): + mock_improve.side_effect = IntegrityError("duplicate key value violates unique constraint") + + self.assertFalse(self.basic_user.has_perm("change_product", self.product1)) + with self.assertLogs(tasks_logger) as cm: + results = improve_packages_from_purldb_task(self.product1.uuid, self.super_user.uuid) + self.assertIsNone(results) + + import_project = self.product1.scancodeprojects.get() + self.assertEqual(import_project.type, ScanCodeProject.ProjectType.IMPROVE_FROM_PURLDB) + self.assertEqual(import_project.status, ScanCodeProject.Status.FAILURE) + expected = ["Error:", "duplicate key value violates unique constraint"] + self.assertEqual(expected, import_project.import_log) + + expected = ( + "ERROR:product_portfolio.tasks:[improve_packages_from_purldb]: " + "duplicate key value violates unique constraint." + ) + self.assertIn(expected, cm.output) diff --git a/product_portfolio/tests/test_views.py b/product_portfolio/tests/test_views.py index 081e0a7d..f47da018 100644 --- a/product_portfolio/tests/test_views.py +++ b/product_portfolio/tests/test_views.py @@ -8,12 +8,12 @@ import io import json -import uuid from unittest import mock from urllib.parse import quote from django.contrib.contenttypes.models import ContentType from django.core.files.base import ContentFile +from django.core.files.uploadedfile import SimpleUploadedFile from django.shortcuts import resolve_url from django.test import TestCase from django.test.utils import override_settings @@ -33,17 +33,15 @@ from dje.models import Dataspace from dje.models import History from dje.outputs import get_spdx_extracted_licenses -from dje.tasks import improve_packages_from_purldb -from dje.tasks import logger as tasks_logger -from dje.tasks import pull_project_data_from_scancodeio -from dje.tasks import scancodeio_submit_project from dje.tests import MaxQueryMixin from dje.tests import add_perms from dje.tests import create_superuser from dje.tests import create_user from license_library.models import License +from license_library.tests import make_license from organization.models import Owner from policy.models import UsagePolicy +from policy.tests import make_usage_policy from product_portfolio.forms import ProductForm from product_portfolio.forms import ProductGridConfigurationForm from product_portfolio.forms import ProductPackageForm @@ -58,6 +56,7 @@ from product_portfolio.tests import make_product from product_portfolio.tests import make_product_dependency from product_portfolio.tests import make_product_package +from product_portfolio.tests import make_product_status from product_portfolio.views import ManageComponentGridView from vulnerabilities.models import VulnerabilityAnalysis from vulnerabilities.tests import make_vulnerability @@ -137,7 +136,7 @@ def test_product_portfolio_detail_view_tab_inventory_and_hierarchy_availability( ProductComponent.objects.create( product=self.product1, component=self.component1, dataspace=self.dataspace ) - with self.assertNumQueries(30): + with self.assertNumQueries(29): response = self.client.get(url) self.assertContains(response, expected1) self.assertContains(response, expected2) @@ -233,6 +232,18 @@ def test_product_portfolio_detail_view_tab_imports_view(self): self.assertNotContains(response, "hx-trigger") self.assertNotContains(response, "Imports are currently in progress.") + expected = "File:" + download_url = reverse( + "product_portfolio:scancodeio_project_download_input", args=[str(project.uuid)] + ) + self.assertNotContains(response, expected) + self.assertNotContains(response, download_url) + project.input_file = ContentFile("Data", name="data.json") + project.save() + response = self.client.get(url) + self.assertContains(response, expected) + self.assertContains(response, download_url) + def test_product_portfolio_detail_view_tab_dependency_view(self): self.client.login(username="nexb_user", password="secret") url = self.product1.get_url("tab_dependencies") @@ -455,6 +466,7 @@ def test_product_portfolio_detail_view_tab_vulnerability_label(self, mock_is_con self.dataspace.enable_vulnerablecodedb_access = True self.dataspace.save() + make_product_package(self.product1) response = self.client.get(url) expected = 'aria-controls="tab_vulnerabilities" aria-selected="false" disabled="disabled"' self.assertContains(response, expected) @@ -1083,13 +1095,40 @@ def test_product_portfolio_detail_edit_productcomponent_permissions(self): response = self.client.get(url) self.assertContains(response, delete_button) + @mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.is_configured") + def test_product_portfolio_detail_view_has_scan_all_packages(self, mock_is_configured): + mock_is_configured.return_value = True + + self.client.login(username=self.basic_user.username, password="secret") + self.assertFalse(self.basic_user.dataspace.enable_package_scanning) + + url = self.product1.get_absolute_url() + response = self.client.get(url) + self.assertEqual(404, response.status_code) + + assign_perm("view_product", self.basic_user, self.product1) + response = self.client.get(url) + self.assertEqual(200, response.status_code) + self.assertFalse(response.context.get("has_scan_all_packages")) + + self.basic_user.dataspace.enable_package_scanning = True + self.basic_user.dataspace.save() + response = self.client.get(url) + self.assertEqual(200, response.status_code) + self.assertFalse(response.context.get("has_scan_all_packages")) + + assign_perm("change_product", self.basic_user, self.product1) + response = self.client.get(url) + self.assertEqual(200, response.status_code) + self.assertTrue(response.context.get("has_scan_all_packages")) + def test_product_portfolio_detail_view_display_purldb_features(self): self.client.login(username=self.super_user.username, password="secret") self.assertFalse(self.super_user.dataspace.enable_purldb_access) url = self.product1.get_absolute_url() expected1 = '' - expected2 = "Improve Packages from PurlDB" + expected2 = "Improve Packages from PurlDB" expected3 = self.product1.get_url("improve_packages_from_purldb") response = self.client.get(url) @@ -1105,6 +1144,31 @@ def test_product_portfolio_detail_view_display_purldb_features(self): self.assertContains(response, expected2) self.assertContains(response, expected3) + def test_product_portfolio_detail_view_status_is_locked(self): + self.client.login(username=self.super_user.username, password="secret") + url = self.product1.get_absolute_url() + expected1 = ( + '" + ) + expected2 = ( + '" + ) + expected3 = ( + "This product version is marked as read-only, preventing any modifications " + "to its inventory." + ) + + locked_status = make_product_status(self.dataspace, is_locked=True) + self.product1.update(configuration_status=locked_status) + response = self.client.get(url) + self.assertContains(response, expected1, html=True) + self.assertContains(response, expected2, html=True) + self.assertContains(response, expected3, html=True) + def test_product_portfolio_list_view_secured_queryset(self): self.client.login(username=self.basic_user.username, password="secret") url = resolve_url("product_portfolio:product_list") @@ -1217,10 +1281,10 @@ def test_product_portfolio_details_view_admin_links(self, mock_is_configured): manage_components_url = self.product1.get_manage_components_url() manage_packages_url = self.product1.get_manage_packages_url() - expected1 = "Scan all Packages" + expected_scan_all = "Scan all Packages" response = self.client.get(url) - self.assertContains(response, expected1, html=True) + self.assertContains(response, expected_scan_all, html=True) self.assertContains(response, manage_components_url) self.assertContains(response, manage_packages_url) @@ -1237,33 +1301,33 @@ def test_product_portfolio_details_view_admin_links(self, mock_is_configured): assign_perm("view_product", self.super_user, self.product1) response = self.client.get(url) - self.assertNotContains(response, expected1, html=True) + self.assertNotContains(response, expected_scan_all, html=True) self.assertNotContains(response, manage_components_url) self.assertNotContains(response, manage_packages_url) perms = ["change_productcomponent"] self.super_user = add_perms(self.super_user, perms) response = self.client.get(url) - self.assertNotContains(response, expected1, html=True) + self.assertNotContains(response, expected_scan_all, html=True) self.assertNotContains(response, manage_components_url) self.assertNotContains(response, manage_packages_url) assign_perm("change_product", self.super_user, self.product1) response = self.client.get(url) - self.assertNotContains(response, expected1, html=True) + self.assertContains(response, expected_scan_all, html=True) self.assertContains(response, manage_components_url) self.assertNotContains(response, manage_packages_url) self.super_user = add_perms(self.super_user, ["change_productpackage"]) response = self.client.get(url) - self.assertNotContains(response, expected1, html=True) + self.assertContains(response, expected_scan_all, html=True) self.assertContains(response, manage_components_url) self.assertContains(response, manage_packages_url) self.super_user.is_superuser = True self.super_user.save() response = self.client.get(url) - self.assertContains(response, expected1, html=True) + self.assertContains(response, expected_scan_all, html=True) def test_product_portfolio_list_view_request_links(self): self.client.login(username="nexb_user", password="secret") @@ -1316,8 +1380,9 @@ def test_product_portfolio_list_view_compare_button(self): url = resolve_url("product_portfolio:product_list") response = self.client.get(url) expected = """ - """ self.assertContains(response, expected, html=True) @@ -1655,22 +1720,24 @@ def test_product_send_about_files_view(self): self.assertEqual(200, response.status_code) self.assertEqual("153", response["content-length"]) - @mock.patch("product_portfolio.views.tasks.scancodeio_submit_scan.delay") + @mock.patch("dje.tasks.scancodeio_submit_scan.delay") @mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.is_configured") def test_product_scan_all_packages_view(self, mock_is_configured, mock_scancodeio_submit_scan): mock_is_configured.return_value = True scan_all_packages_url = self.product1.get_scan_all_packages_url() response = self.client.get(scan_all_packages_url) + self.assertEqual(405, response.status_code) + response = self.client.post(scan_all_packages_url) self.assertEqual(302, response.status_code) self.client.login(username=self.super_user.username, password="secret") - response = self.client.get(scan_all_packages_url) + response = self.client.post(scan_all_packages_url) self.assertEqual(404, response.status_code) self.super_user.dataspace.enable_package_scanning = True self.super_user.dataspace.save() - response = self.client.get(scan_all_packages_url) + response = self.client.post(scan_all_packages_url) self.assertEqual(404, response.status_code) self.package1.download_url = "https://proper-url.com" @@ -1687,7 +1754,7 @@ def test_product_scan_all_packages_view(self, mock_is_configured, mock_scancodei self.assertTrue(len(self.product1.all_packages)) with self.captureOnCommitCallbacks(execute=True) as callbacks: - response = self.client.get(scan_all_packages_url, follow=True) + response = self.client.post(scan_all_packages_url, follow=True) self.assertRedirects(response, self.product1.get_absolute_url()) self.assertContains(response, "Click here to see the Scans list.") @@ -1700,11 +1767,15 @@ def test_product_scan_all_packages_view(self, mock_is_configured, mock_scancodei dataspace_uuid=self.super_user.dataspace.uuid, ) - self.super_user.is_superuser = False - self.super_user.save() - response = self.client.get(scan_all_packages_url) + self.client.login(username=self.basic_user.username, password="secret") + response = self.client.post(scan_all_packages_url) self.assertEqual(404, response.status_code) + assign_perm("view_product", self.basic_user, self.product1) + assign_perm("change_product", self.basic_user, self.product1) + response = self.client.post(scan_all_packages_url) + self.assertRedirects(response, self.product1.get_absolute_url()) + def test_product_portfolio_product_add_view_permission_access(self): add_url = reverse("product_portfolio:product_add") response = self.client.get(add_url) @@ -2613,6 +2684,45 @@ def test_product_portfolio_product_manage_packages_grid_view_permissions(self): response = self.client.get(manage_url) self.assertEqual(200, response.status_code) + def test_product_portfolio_product_manage_packages_grid_fields_permissions(self): + add_perms(self.basic_user, ["change_productpackage"]) + assign_perm("view_product", self.basic_user, self.product1) + assign_perm("change_product", self.basic_user, self.product1) + self.client.login(username=self.basic_user.username, password="secret") + + make_product_package(self.product1) + manage_url = self.product1.get_manage_packages_url() + response = self.client.get(manage_url) + + self.assertEqual(200, response.status_code) + expected = ( + '" + ) + self.assertContains(response, expected, html=True) + form = response.context["formset"].forms[0] + self.assertIn("review_status", form.fields) + self.assertTrue(form.fields["review_status"].disabled) + + data = { + "form-TOTAL_FORMS": 1, + "form-INITIAL_FORMS": 0, + "form-MIN_NUM_FORMS": 0, + "form-MAX_NUM_FORMS": 1000, + "form-0-product": self.product1.pk, + "form-0-package": self.package1.pk, + "form-0-object_display": str(self.package1), + "form-0-review_status": "PROTECTED FIELD", + "form-0-notes": "Some notes", + } + response = self.client.post(manage_url, data, follow=True) + self.assertContains(response, "Product changes saved.") + self.assertRedirects(response, manage_url) + pp2 = ProductPackage.objects.get(product=self.product1, package=self.package1.pk) + self.assertIsNone(pp2.review_status) + def test_product_portfolio_product_manage_packages_grid_view_delete(self): self.client.login(username=self.basic_user.username, password="secret") @@ -2952,9 +3062,7 @@ def test_product_portfolio_product_export_cyclonedx_view(self): self.client.login(username=self.super_user.username, password="secret") export_cyclonedx_url = self.product1.get_export_cyclonedx_url() response = self.client.get(export_cyclonedx_url) - self.assertEqual( - "dejacode_nexb_product_product1_with_space_1.0.cdx.json", response.filename - ) + self.assertEqual("dejacode_nexb_product1_with_space_1.0.cdx.json", response.filename) self.assertEqual("application/json", response.headers["Content-Type"]) content = io.BytesIO(b"".join(response.streaming_content)) @@ -3033,45 +3141,17 @@ def test_product_portfolio_product_export_cyclonedx_view(self): self.assertIn("vulnerabilities", response_str) self.assertIn(vulnerability1.vulnerability_id, response_str) - @mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.submit_project") - def test_scancodeio_submit_project_task(self, mock_submit_project): - scancodeproject = ScanCodeProject.objects.create( - product=self.product1, - dataspace=self.product1.dataspace, - type=ScanCodeProject.ProjectType.LOAD_SBOMS, - input_file=ContentFile("Data", name="data.json"), - ) - - mock_submit_project.return_value = None - scancodeio_submit_project( - scancodeproject_uuid=scancodeproject.uuid, - user_uuid=self.super_user.uuid, - pipeline_name="load_sboms", - ) - scancodeproject.refresh_from_db() - self.assertEqual("failure", scancodeproject.status) - self.assertIsNone(scancodeproject.project_uuid) - expected = ["- Error: File could not be submitted to ScanCode.io"] - self.assertEqual(expected, scancodeproject.import_log) - - # Reset the instance values - scancodeproject.status = "" - scancodeproject.import_log = [] - scancodeproject.save() - - project_uuid = uuid.uuid4() - mock_submit_project.return_value = {"uuid": project_uuid} - scancodeio_submit_project( - scancodeproject_uuid=scancodeproject.uuid, - user_uuid=self.super_user.uuid, - pipeline_name="load_sboms", - ) + def test_product_portfolio_product_export_openvex_view(self): + self.client.login(username=self.super_user.username, password="secret") + export_openvex_url = self.product1.get_export_openvex_url() + response = self.client.get(export_openvex_url) + self.assertEqual("dejacode_nexb_product1_with_space_1.0.openvex.json", response.filename) + self.assertEqual("application/json", response.headers["Content-Type"]) - scancodeproject.refresh_from_db() - self.assertEqual("submitted", scancodeproject.status) - self.assertEqual(project_uuid, scancodeproject.project_uuid) - expected = ["- File submitted to ScanCode.io for inspection"] - self.assertEqual(expected, scancodeproject.import_log) + content = io.BytesIO(b"".join(response.streaming_content)) + bom_as_dict = json.loads(content.read().decode("utf-8")) + self.assertEqual("https://openvex.dev/ns/v0.2.0", bom_as_dict["@context"]) + self.assertEqual(self.dataspace.name, bom_as_dict["author"]) @mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.submit_project") def test_product_portfolio_load_sbom_view(self, mock_submit): @@ -3233,75 +3313,6 @@ def test_product_portfolio_pull_project_data_from_scancodeio_view(self, mock_fin self.assertEqual(ScanCodeProject.Status.SUBMITTED, project.status) self.assertEqual(self.super_user, project.created_by) - @mock.patch("product_portfolio.models.ScanCodeProject.import_data_from_scancodeio") - def test_product_portfolio_pull_project_data_from_scancodeio_task(self, mock_import_data): - scancode_project = ScanCodeProject.objects.create( - product=self.product1, - dataspace=self.product1.dataspace, - type=ScanCodeProject.ProjectType.PULL_FROM_SCANCODEIO, - created_by=self.super_user, - ) - - mock_import_data.side_effect = Exception("Error") - pull_project_data_from_scancodeio(scancodeproject_uuid=scancode_project.uuid) - scancode_project.refresh_from_db() - self.assertEqual(ScanCodeProject.Status.FAILURE, scancode_project.status) - self.assertEqual(["Error"], scancode_project.import_log) - notif = Notification.objects.get() - self.assertTrue(notif.unread) - self.assertEqual(self.super_user, notif.actor) - self.assertEqual("Import packages from ScanCode.io", notif.verb) - self.assertEqual(self.product1, notif.action_object) - self.assertEqual(self.super_user, notif.recipient) - self.assertEqual("Import failed.", notif.description) - - Notification.objects.all().delete() - scancode_project.import_log = [] - scancode_project.status = ScanCodeProject.Status.SUBMITTED - scancode_project.save() - mock_import_data.side_effect = None - mock_import_data.return_value = ( - {"package": ["package1"]}, - {"package": ["package2"]}, - {"package": ["error1"]}, - ) - pull_project_data_from_scancodeio(scancodeproject_uuid=scancode_project.uuid) - scancode_project.refresh_from_db() - self.assertEqual(ScanCodeProject.Status.SUCCESS, scancode_project.status) - expected = [ - "- Imported 1 package.", - "- 1 package already available in the Dataspace.", - "- 1 package error occurred during import.", - ] - self.assertEqual(expected, scancode_project.import_log) - - notif = Notification.objects.get() - self.assertTrue(notif.unread) - self.assertEqual(self.super_user, notif.actor) - self.assertEqual("Import packages from ScanCode.io", notif.verb) - self.assertEqual(self.product1, notif.action_object) - self.assertEqual(self.super_user, notif.recipient) - self.assertEqual("\n".join(scancode_project.import_log), notif.description) - - def test_product_portfolio_pull_project_data_from_scancodeio_task_can_start_import(self): - scancode_project = ScanCodeProject.objects.create( - product=self.product1, - dataspace=self.product1.dataspace, - type=ScanCodeProject.ProjectType.PULL_FROM_SCANCODEIO, - status=ScanCodeProject.Status.IMPORT_STARTED, - created_by=self.super_user, - ) - - with self.assertLogs(tasks_logger) as cm: - pull_project_data_from_scancodeio(scancodeproject_uuid=scancode_project.uuid) - - expected = [ - f"INFO:dje.tasks:Entering pull_project_data_from_scancodeio task with " - f"scancodeproject_uuid={scancode_project.uuid}", - "ERROR:dje.tasks:Cannot start import", - ] - self.assertEqual(expected, cm.output) - @mock.patch("dejacode_toolkit.purldb.PurlDB.is_configured") def test_product_portfolio_improve_packages_from_purldb_view(self, mock_is_configured): mock_is_configured.return_value = True @@ -3332,52 +3343,44 @@ def test_product_portfolio_improve_packages_from_purldb_view(self, mock_is_confi self.assertEqual(200, response.status_code) self.assertContains(response, "Improve Packages already in progress...") - @mock.patch("product_portfolio.models.Product.improve_packages_from_purldb") - def test_product_portfolio_improve_packages_from_purldb_task(self, mock_improve): - mock_improve.return_value = ["pkg1", "pkg2"] - - self.assertFalse(self.basic_user.has_perm("change_product", self.product1)) - with self.assertLogs(tasks_logger) as cm: - improve_packages_from_purldb(self.product1.uuid, self.basic_user.uuid) - - expected = ( - "ERROR:dje.tasks:[improve_packages_from_purldb]: " - f"Product uuid={self.product1.uuid} does not exists." + def test_product_portfolio_scancodeio_project_download_input_view(self): + test_file_content = b"dummy input file content" + test_file = SimpleUploadedFile( + "input.zip", test_file_content, content_type="application/zip" ) - self.assertIn(expected, cm.output) - - assign_perm("view_product", self.basic_user, self.product1) - self.assertFalse(self.basic_user.has_perm("change_product", self.product1)) - with self.assertLogs(tasks_logger) as cm: - improve_packages_from_purldb(self.product1.uuid, self.basic_user.uuid) - expected = "ERROR:dje.tasks:[improve_packages_from_purldb]: Permission denied." - self.assertIn(expected, cm.output) + # Create a ScanCodeProject with file + scancode_project = ScanCodeProject.objects.create( + product=self.product1, + dataspace=self.product1.dataspace, + input_file=test_file, + type=ScanCodeProject.ProjectType.LOAD_SBOMS, + status=ScanCodeProject.Status.SUCCESS, + ) - self.assertTrue(self.super_user.has_perm("change_product", self.product1)) - with self.assertLogs(tasks_logger) as cm: - improve_packages_from_purldb(self.product1.uuid, self.super_user.uuid) + download_url = reverse( + "product_portfolio:scancodeio_project_download_input", args=[str(scancode_project.uuid)] + ) - mock_improve.assert_called_once() - expected = [ - "INFO:dje.tasks:Entering improve_packages_from_purldb task with " - f"product_uuid={self.product1.uuid} " - f"user_uuid={self.super_user.uuid}", - "INFO:dje.tasks:[improve_packages_from_purldb]: 2 updated from PurlDB.", - ] - self.assertEqual(expected, cm.output) + # No permission initially + self.client.login(username=self.basic_user.username, password="secret") + response = self.client.get(download_url) + self.assertEqual(response.status_code, 404) - import_project = self.product1.scancodeprojects.get() - self.assertEqual(import_project.type, ScanCodeProject.ProjectType.IMPROVE_FROM_PURLDB) - self.assertEqual(import_project.status, ScanCodeProject.Status.SUCCESS) - expected = ["Improved packages from PurlDB:", "pkg1, pkg2"] - self.assertEqual(expected, import_project.import_log) + # Grant permission + assign_perm("view_product", self.basic_user, self.product1) + response = self.client.get(download_url) + self.assertEqual(response.status_code, 200) + downloaded_content = b"".join(response.streaming_content) + self.assertEqual(test_file_content, downloaded_content) + self.assertEqual( + response["Content-Disposition"], f'attachment; filename="{test_file.name}"' + ) - notification = Notification.objects.get() - self.assertEqual("Improved packages from PurlDB:", notification.verb) - self.assertEqual("pkg1, pkg2", notification.description) - self.assertEqual("dejacodeuser", notification.actor_content_type.model) - self.assertEqual(self.product1, notification.action_object) + # Remove the file and test for 404 + scancode_project.input_file.delete(save=True) + response = self.client.get(download_url) + self.assertEqual(response.status_code, 404) def test_product_portfolio_vulnerability_analysis_form_view(self): self.client.login(username=self.super_user.username, password="secret") @@ -3412,3 +3415,837 @@ def test_product_portfolio_vulnerability_analysis_form_view(self): self.assertEqual(product_package, analysis.product_package) self.assertEqual(vulnerability1, analysis.vulnerability) self.assertEqual("resolved", analysis.state) + + def test_product_portfolio_tab_compliance_view_empty(self): + self.client.login(username="nexb_user", password="secret") + url = self.product1.get_url("tab_compliance") + response = self.client.get(url) + self.assertEqual(200, response.status_code) + self.assertEqual(0, response.context["total_packages"]) + self.assertEqual(100, response.context["license_compliance_pct"]) + self.assertEqual(100, response.context["license_coverage_pct"]) + self.assertEqual(0, response.context["vulnerability_count"]) + + def test_product_portfolio_tab_compliance_view_package_compliance(self): + self.client.login(username="nexb_user", password="secret") + + owner1 = Owner.objects.create(name="Owner1", dataspace=self.dataspace) + license1 = License.objects.create( + key="l1", name="L1", short_name="L1", owner=owner1, dataspace=self.dataspace + ) + package_policy = UsagePolicy.objects.create( + label="PackagePolicy", + icon="icon", + content_type=ContentType.objects.get_for_model(Package), + compliance_alert=UsagePolicy.Compliance.ERROR, + dataspace=self.dataspace, + ) + + package2 = make_package(self.dataspace, usage_policy=package_policy) + package3 = make_package(self.dataspace) + + # package2 has a policy issue, package3 does not + ProductPackage.objects.create( + product=self.product1, + package=package2, + dataspace=self.dataspace, + license_expression=license1.key, + ) + ProductPackage.objects.create( + product=self.product1, + package=package3, + dataspace=self.dataspace, + ) + + url = self.product1.get_url("tab_compliance") + response = self.client.get(url) + self.assertEqual(2, response.context["total_packages"]) + self.assertEqual(1, response.context["package_issues_count"]) + # One package without license expression + self.assertEqual(1, response.context["package_without_license_count"]) + self.assertEqual(50, response.context["license_coverage_pct"]) + + def test_product_portfolio_tab_compliance_view_license_compliance(self): + self.client.login(username="nexb_user", password="secret") + + owner1 = Owner.objects.create(name="Owner1", dataspace=self.dataspace) + license_policy_error = UsagePolicy.objects.create( + label="LicensePolicyError", + icon="icon", + content_type=ContentType.objects.get_for_model(License), + compliance_alert=UsagePolicy.Compliance.ERROR, + dataspace=self.dataspace, + ) + license_policy_warning = UsagePolicy.objects.create( + label="LicensePolicyWarning", + icon="icon", + content_type=ContentType.objects.get_for_model(License), + compliance_alert=UsagePolicy.Compliance.WARNING, + dataspace=self.dataspace, + ) + license1 = License.objects.create( + key="l1", + name="L1", + short_name="L1", + owner=owner1, + usage_policy=license_policy_error, + dataspace=self.dataspace, + ) + license2 = License.objects.create( + key="l2", + name="L2", + short_name="L2", + owner=owner1, + usage_policy=license_policy_warning, + dataspace=self.dataspace, + ) + + package2 = make_package(self.dataspace) + package3 = make_package(self.dataspace) + ProductPackage.objects.create( + product=self.product1, + package=package2, + dataspace=self.dataspace, + license_expression=license1.key, + ) + ProductPackage.objects.create( + product=self.product1, + package=package3, + dataspace=self.dataspace, + license_expression=license2.key, + ) + + url = self.product1.get_url("tab_compliance") + response = self.client.get(url) + self.assertEqual(2, response.context["license_issues_count"]) + self.assertEqual(1, response.context["license_error_count"]) + self.assertEqual(1, response.context["license_warning_count"]) + # Both packages have license issues, so 0% compliance + self.assertEqual(0, response.context["license_compliance_pct"]) + + def test_product_portfolio_tab_compliance_view_security_compliance(self): + self.client.login(username="nexb_user", password="secret") + + p1 = make_package(self.dataspace) + p2 = make_package(self.dataspace) + p3 = make_package(self.dataspace) + make_vulnerability(self.dataspace, affecting=[p1], risk_score=9.0) + make_vulnerability(self.dataspace, affecting=[p2], risk_score=6.5) + make_vulnerability(self.dataspace, affecting=[p3], risk_score=2.0) + + product1 = make_product(self.dataspace, inventory=[p1, p2, p3]) + url = product1.get_url("tab_compliance") + response = self.client.get(url) + + self.assertEqual(3, response.context["vulnerability_count"]) + self.assertEqual(3, response.context["above_threshold_count"]) + self.assertEqual("critical", response.context["max_vulnerability_severity"]) + self.assertEqual(1, response.context["critical_count"]) + self.assertEqual(1, response.context["high_count"]) + self.assertEqual(0, response.context["medium_count"]) + self.assertEqual(1, response.context["low_count"]) + + def test_product_portfolio_tab_compliance_view_security_with_risk_threshold(self): + self.client.login(username="nexb_user", password="secret") + + p1 = make_package(self.dataspace) + p2 = make_package(self.dataspace) + make_vulnerability(self.dataspace, affecting=[p1], risk_score=9.0) + make_vulnerability(self.dataspace, affecting=[p2], risk_score=2.0) + + product1 = make_product(self.dataspace, inventory=[p1, p2]) + product1.update(vulnerabilities_risk_threshold=6.0) + + url = product1.get_url("tab_compliance") + response = self.client.get(url) + + self.assertEqual(2, response.context["vulnerability_count"]) + self.assertEqual(1, response.context["above_threshold_count"]) + self.assertEqual("high", response.context["risk_threshold"]) + self.assertEqual(6.0, response.context["risk_threshold_number"]) + + def test_product_portfolio_compliance_dashboard_view_access(self): + url = reverse("product_portfolio:compliance_dashboard") + response = self.client.get(url) + self.assertRedirects(response, f"/login/?next={url}") + + self.client.login(username=self.basic_user.username, password="secret") + response = self.client.get(url) + self.assertEqual(200, response.status_code) + self.assertContains(response, "Compliance Control Center") + + def test_product_portfolio_compliance_dashboard_view_empty(self): + self.client.login(username=self.basic_user.username, password="secret") + url = reverse("product_portfolio:compliance_dashboard") + response = self.client.get(url) + self.assertEqual(0, response.context["total_products"]) + self.assertEqual(0, response.context["products_with_issues"]) + self.assertEqual(0, response.context["total_vulnerabilities"]) + self.assertContains(response, "No active products") + + def test_product_portfolio_compliance_dashboard_view_product_visibility(self): + self.client.login(username=self.basic_user.username, password="secret") + url = reverse("product_portfolio:compliance_dashboard") + + response = self.client.get(url) + self.assertEqual(0, response.context["total_products"]) + + assign_perm("view_product", self.basic_user, self.product1) + response = self.client.get(url) + self.assertEqual(1, response.context["total_products"]) + + def test_product_portfolio_compliance_dashboard_view_excludes_inactive(self): + self.client.login(username=self.super_user.username, password="secret") + url = reverse("product_portfolio:compliance_dashboard") + + response = self.client.get(url) + self.assertIn(self.product1, response.context["object_list"]) + + self.product1.is_active = False + self.product1.save() + response = self.client.get(url) + self.assertNotIn(self.product1, response.context["object_list"]) + + def test_product_portfolio_compliance_dashboard_view_excludes_locked(self): + self.client.login(username=self.super_user.username, password="secret") + url = reverse("product_portfolio:compliance_dashboard") + + response = self.client.get(url) + self.assertIn(self.product1, response.context["object_list"]) + + locked_status = make_product_status(self.dataspace, is_locked=True) + self.product1.update(configuration_status=locked_status) + response = self.client.get(url) + self.assertNotIn(self.product1, response.context["object_list"]) + + def test_product_portfolio_compliance_dashboard_view_license_issues(self): + self.client.login(username=self.super_user.username, password="secret") + url = reverse("product_portfolio:compliance_dashboard") + + owner1 = Owner.objects.create(name="Owner1", dataspace=self.dataspace) + license_policy = UsagePolicy.objects.create( + label="LicensePolicy", + icon="icon", + content_type=ContentType.objects.get_for_model(License), + compliance_alert=UsagePolicy.Compliance.ERROR, + dataspace=self.dataspace, + ) + license1 = License.objects.create( + key="l1", + name="L1", + short_name="L1", + owner=owner1, + usage_policy=license_policy, + dataspace=self.dataspace, + ) + package1 = make_package(self.dataspace) + ProductPackage.objects.create( + product=self.product1, + package=package1, + dataspace=self.dataspace, + license_expression=license1.key, + ) + + response = self.client.get(url) + self.assertEqual(1, response.context["products_with_license_issues"]) + self.assertEqual(1, response.context["products_with_issues"]) + + def test_product_portfolio_compliance_dashboard_view_vulnerability_counts(self): + self.client.login(username=self.super_user.username, password="secret") + url = reverse("product_portfolio:compliance_dashboard") + + p1 = make_package(self.dataspace) + p2 = make_package(self.dataspace) + make_vulnerability(self.dataspace, affecting=[p1], risk_score=9.0) + make_vulnerability(self.dataspace, affecting=[p2], risk_score=6.5) + + make_product(self.dataspace, inventory=[p1, p2]) + + response = self.client.get(url) + self.assertEqual(1, response.context["products_with_critical_or_high"]) + self.assertEqual(2, response.context["total_vulnerabilities"]) + self.assertEqual(1, response.context["total_critical"]) + self.assertEqual(1, response.context["total_high"]) + self.assertEqual(0, response.context["total_medium"]) + self.assertEqual(0, response.context["total_low"]) + + def test_product_portfolio_compliance_dashboard_view_ordering(self): + self.client.login(username=self.super_user.username, password="secret") + url = reverse("product_portfolio:compliance_dashboard") + + p1 = make_package(self.dataspace) + p2 = make_package(self.dataspace) + make_vulnerability(self.dataspace, affecting=[p1], risk_score=9.0) + make_vulnerability(self.dataspace, affecting=[p2], risk_score=3.0) + + product_critical = make_product(self.dataspace, inventory=[p1]) + product_medium = make_product(self.dataspace, inventory=[p2]) + + response = self.client.get(url) + products = list(response.context["object_list"]) + critical_index = products.index(product_critical) + medium_index = products.index(product_medium) + self.assertLess(critical_index, medium_index) + + def test_product_portfolio_compliance_dashboard_view_pagination(self): + self.client.login(username=self.super_user.username, password="secret") + url = reverse("product_portfolio:compliance_dashboard") + + response = self.client.get(url) + self.assertFalse(response.context["is_paginated"]) + + for index in range(55): + Product.objects.create( + name=f"PaginationProduct{index}", + version="1.0", + dataspace=self.dataspace, + ) + + response = self.client.get(url) + self.assertTrue(response.context["is_paginated"]) + self.assertEqual(50, len(response.context["object_list"])) + + response = self.client.get(url + "?page=2") + self.assertEqual(7, len(response.context["object_list"])) + + def test_product_portfolio_compliance_dashboard_view_risk_threshold_product(self): + """Vulnerability counts respect per-product risk threshold.""" + self.client.login(username=self.super_user.username, password="secret") + url = reverse("product_portfolio:compliance_dashboard") + + p1 = make_package(self.dataspace) + p2 = make_package(self.dataspace) + p3 = make_package(self.dataspace) + make_vulnerability(self.dataspace, affecting=[p1], risk_score=9.0) + make_vulnerability(self.dataspace, affecting=[p2], risk_score=6.5) + make_vulnerability(self.dataspace, affecting=[p3], risk_score=2.0) + + product1 = make_product(self.dataspace, inventory=[p1, p2, p3]) + + # No threshold: all 3 vulns count + response = self.client.get(url) + product_in_list = [p for p in response.context["object_list"] if p.pk == product1.pk][0] + self.assertEqual(3, product_in_list.vulnerability_count) + self.assertEqual(1, product_in_list.critical_count) + self.assertEqual(1, product_in_list.high_count) + self.assertEqual(0, product_in_list.medium_count) + self.assertEqual(1, product_in_list.low_count) + + # Set threshold to 6.0: only critical and high count + product1.update(vulnerabilities_risk_threshold=6.0) + response = self.client.get(url) + product_in_list = [p for p in response.context["object_list"] if p.pk == product1.pk][0] + self.assertEqual(2, product_in_list.vulnerability_count) + self.assertEqual(1, product_in_list.critical_count) + self.assertEqual(1, product_in_list.high_count) + self.assertEqual(0, product_in_list.medium_count) + self.assertEqual(0, product_in_list.low_count) + + # Set threshold to 8.0: only critical counts + product1.update(vulnerabilities_risk_threshold=8.0) + response = self.client.get(url) + product_in_list = [p for p in response.context["object_list"] if p.pk == product1.pk][0] + self.assertEqual(1, product_in_list.vulnerability_count) + self.assertEqual(1, product_in_list.critical_count) + self.assertEqual(0, product_in_list.high_count) + + def test_product_portfolio_compliance_dashboard_view_risk_threshold_dataspace(self): + """Vulnerability counts fall back to dataspace threshold.""" + from dje.models import DataspaceConfiguration + + self.client.login(username=self.super_user.username, password="secret") + url = reverse("product_portfolio:compliance_dashboard") + + p1 = make_package(self.dataspace) + p2 = make_package(self.dataspace) + make_vulnerability(self.dataspace, affecting=[p1], risk_score=9.0) + make_vulnerability(self.dataspace, affecting=[p2], risk_score=2.0) + + make_product(self.dataspace, inventory=[p1, p2]) + + # No threshold anywhere: all vulns count + response = self.client.get(url) + self.assertEqual(2, response.context["total_vulnerabilities"]) + + # Set dataspace threshold to 6.0: only critical counts + DataspaceConfiguration.objects.create( + dataspace=self.dataspace, + vulnerabilities_risk_threshold=6.0, + ) + response = self.client.get(url) + self.assertEqual(1, response.context["total_vulnerabilities"]) + self.assertEqual(1, response.context["total_critical"]) + self.assertEqual(0, response.context["total_low"]) + + def test_product_portfolio_compliance_dashboard_view_risk_threshold_product_overrides_dataspace( + self, + ): + """Product threshold takes precedence over dataspace threshold.""" + from dje.models import DataspaceConfiguration + + self.client.login(username=self.super_user.username, password="secret") + url = reverse("product_portfolio:compliance_dashboard") + + p1 = make_package(self.dataspace) + p2 = make_package(self.dataspace) + p3 = make_package(self.dataspace) + make_vulnerability(self.dataspace, affecting=[p1], risk_score=9.0) + make_vulnerability(self.dataspace, affecting=[p2], risk_score=5.0) + make_vulnerability(self.dataspace, affecting=[p3], risk_score=2.0) + + product1 = make_product(self.dataspace, inventory=[p1, p2, p3]) + + # Dataspace threshold at 6.0 + DataspaceConfiguration.objects.create( + dataspace=self.dataspace, + vulnerabilities_risk_threshold=6.0, + ) + response = self.client.get(url) + product_in_list = [p for p in response.context["object_list"] if p.pk == product1.pk][0] + self.assertEqual(1, product_in_list.vulnerability_count) + + # Product threshold at 3.0 overrides dataspace + product1.update(vulnerabilities_risk_threshold=3.0) + response = self.client.get(url) + product_in_list = [p for p in response.context["object_list"] if p.pk == product1.pk][0] + self.assertEqual(2, product_in_list.vulnerability_count) + + def test_product_portfolio_compliance_dashboard_view_risk_threshold_totals(self): + """Summary totals respect per-product thresholds.""" + self.client.login(username=self.super_user.username, password="secret") + url = reverse("product_portfolio:compliance_dashboard") + + p1 = make_package(self.dataspace) + p2 = make_package(self.dataspace) + p3 = make_package(self.dataspace) + p4 = make_package(self.dataspace) + make_vulnerability(self.dataspace, affecting=[p1], risk_score=9.0) + make_vulnerability(self.dataspace, affecting=[p2], risk_score=2.0) + make_vulnerability(self.dataspace, affecting=[p3], risk_score=8.5) + make_vulnerability(self.dataspace, affecting=[p4], risk_score=1.0) + + # Product A: threshold 6.0, only p1 (9.0) counts + product_a = make_product(self.dataspace, inventory=[p1, p2]) + product_a.update(vulnerabilities_risk_threshold=6.0) + + # Product B: no threshold, both p3 (8.5) and p4 (1.0) count + make_product(self.dataspace, inventory=[p3, p4]) + + response = self.client.get(url) + # Product A: 1 (critical), Product B: 2 (critical + low) + self.assertEqual(3, response.context["total_vulnerabilities"]) + self.assertEqual(2, response.context["total_critical"]) + self.assertEqual(0, response.context["total_high"]) + self.assertEqual(1, response.context["total_low"]) + + def test_product_portfolio_compliance_dashboard_view_risk_threshold_display(self): + """Risk threshold value is displayed in the template.""" + self.client.login(username=self.super_user.username, password="secret") + url = reverse("product_portfolio:compliance_dashboard") + + p1 = make_package(self.dataspace) + make_vulnerability(self.dataspace, affecting=[p1], risk_score=9.0) + product1 = make_product(self.dataspace, inventory=[p1]) + product1.update(vulnerabilities_risk_threshold=7.0) + + response = self.client.get(url) + self.assertContains(response, "7.0") + self.assertContains(response, "Risk threshold") + + def test_product_portfolio_compliance_dashboard_view_export_csv(self): + self.client.login(username=self.super_user.username, password="secret") + url = reverse("product_portfolio:compliance_dashboard") + + p1 = make_package(self.dataspace) + make_vulnerability(self.dataspace, affecting=[p1], risk_score=9.0) + make_product(self.dataspace, inventory=[p1]) + + response = self.client.get(url + "?export=csv") + self.assertEqual(200, response.status_code) + self.assertEqual("text/csv", response["Content-Type"]) + self.assertIn("compliance_dashboard_", response["Content-Disposition"]) + self.assertIn(".csv", response["Content-Disposition"]) + + content = response.content.decode() + self.assertIn("Product,Version,Packages", content) + self.assertIn("critical", content) + + def test_product_portfolio_compliance_dashboard_view_export_xlsx(self): + self.client.login(username=self.super_user.username, password="secret") + url = reverse("product_portfolio:compliance_dashboard") + + make_product_package(self.product1) + + response = self.client.get(url + "?export=xlsx") + self.assertEqual(200, response.status_code) + expected_type = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + self.assertEqual(expected_type, response["Content-Type"]) + self.assertIn("compliance_dashboard_", response["Content-Disposition"]) + self.assertIn(".xlsx", response["Content-Disposition"]) + + def test_product_portfolio_compliance_dashboard_view_export_json(self): + self.client.login(username=self.super_user.username, password="secret") + url = reverse("product_portfolio:compliance_dashboard") + + p1 = make_package(self.dataspace) + make_vulnerability(self.dataspace, affecting=[p1], risk_score=9.0) + product1 = make_product(self.dataspace, inventory=[p1]) + + response = self.client.get(url + "?export=json") + self.assertEqual(200, response.status_code) + self.assertEqual("application/json", response["Content-Type"]) + self.assertIn("compliance_dashboard_", response["Content-Disposition"]) + self.assertIn(".json", response["Content-Disposition"]) + + data = json.loads(response.content) + self.assertTrue(len(data) > 0) + first = next(entry for entry in data if entry["name"] == product1.name) + self.assertEqual(1, first["critical_count"]) + + def test_product_portfolio_compliance_dashboard_view_export_ods(self): + self.client.login(username=self.super_user.username, password="secret") + url = reverse("product_portfolio:compliance_dashboard") + + make_product_package(self.product1) + + response = self.client.get(url + "?export=ods") + self.assertEqual(200, response.status_code) + expected_type = "application/vnd.oasis.opendocument.spreadsheet" + self.assertEqual(expected_type, response["Content-Type"]) + self.assertIn("compliance_dashboard_", response["Content-Disposition"]) + self.assertIn(".ods", response["Content-Disposition"]) + + def test_product_portfolio_compliance_dashboard_view_export_yaml(self): + self.client.login(username=self.super_user.username, password="secret") + url = reverse("product_portfolio:compliance_dashboard") + + p1 = make_package(self.dataspace) + make_vulnerability(self.dataspace, affecting=[p1], risk_score=9.0) + product1 = make_product(self.dataspace, inventory=[p1]) + + response = self.client.get(url + "?export=yaml") + self.assertEqual(200, response.status_code) + self.assertEqual("application/x-yaml", response["Content-Type"]) + self.assertIn("compliance_dashboard_", response["Content-Disposition"]) + self.assertIn(".yaml", response["Content-Disposition"]) + + content = response.content.decode() + self.assertIn(product1.name, content) + self.assertIn("critical_count", content) + + def test_product_portfolio_compliance_dashboard_view_export_respects_permissions(self): + self.client.login(username=self.basic_user.username, password="secret") + url = reverse("product_portfolio:compliance_dashboard") + + p1 = make_package(self.dataspace) + make_vulnerability(self.dataspace, affecting=[p1], risk_score=9.0) + product1 = make_product(self.dataspace, inventory=[p1]) + + # Without permission, export should return empty data + response = self.client.get(url + "?export=json") + self.assertEqual(200, response.status_code) + data = json.loads(response.content) + product_names = [entry["name"] for entry in data] + self.assertNotIn(product1.name, product_names) + + # With permission, product should appear + assign_perm("view_product", self.basic_user, product1) + response = self.client.get(url + "?export=json") + data = json.loads(response.content) + product_names = [entry["name"] for entry in data] + self.assertIn(product1.name, product_names) + + def test_product_portfolio_compliance_dashboard_view_export_invalid_format(self): + self.client.login(username=self.super_user.username, password="secret") + url = reverse("product_portfolio:compliance_dashboard") + + response = self.client.get(url + "?export=pdf") + self.assertEqual(200, response.status_code) + # Invalid format falls through to normal HTML view + self.assertContains(response, "Compliance Control Center") + + def test_product_portfolio_compliance_dashboard_view_export_filename_has_timestamp(self): + self.client.login(username=self.super_user.username, password="secret") + url = reverse("product_portfolio:compliance_dashboard") + + response = self.client.get(url + "?export=csv") + disposition = response["Content-Disposition"] + # Format: compliance_dashboard_YYYY-MM-DD_HHMMSS.csv + self.assertRegex( + disposition, + r"compliance_dashboard_\d{4}-\d{2}-\d{2}_\d{6}\.csv", + ) + + def test_product_portfolio_compliance_dashboard_view_export_risk_threshold(self): + self.client.login(username=self.super_user.username, password="secret") + url = reverse("product_portfolio:compliance_dashboard") + + p1 = make_package(self.dataspace) + p2 = make_package(self.dataspace) + make_vulnerability(self.dataspace, affecting=[p1], risk_score=9.0) + make_vulnerability(self.dataspace, affecting=[p2], risk_score=2.0) + + product1 = make_product(self.dataspace, inventory=[p1, p2]) + product1.update(vulnerabilities_risk_threshold=6.0) + + response = self.client.get(url + "?export=json") + data = json.loads(response.content) + product_data = next(entry for entry in data if entry["name"] == product1.name) + # Only the critical vulnerability (9.0) should count, low (2.0) is below threshold + self.assertEqual(1, product_data["vulnerability_count"]) + self.assertEqual(1, product_data["critical_count"]) + self.assertEqual(0, product_data["low_count"]) + + def test_product_portfolio_product_license_compliance_export_view_csv(self): + self.client.login(username=self.super_user.username, password="secret") + url = self.product1.get_export_license_compliance_url() + + usage_policy = make_usage_policy( + self.dataspace, + model=License, + compliance_alert="error", + ) + license1 = make_license( + self.dataspace, + key="mit", + short_name="MIT", + spdx_license_key="MIT", + usage_policy=usage_policy, + ) + make_product_package(self.product1, license_expression=license1.key) + + response = self.client.get(url + "?export=csv") + self.assertEqual(200, response.status_code) + self.assertEqual("text/csv", response["Content-Type"]) + self.assertIn("license_compliance_", response["Content-Disposition"]) + self.assertIn(".csv", response["Content-Disposition"]) + # Filename includes the product since the view carries a detail object + self.assertIn("product1_with_space", response["Content-Disposition"]) + + content = response.content.decode() + self.assertIn("SPDX license key,Short name,Key,Packages,Compliance alert", content) + self.assertIn("MIT,MIT,mit,1,error", content) + + def test_product_portfolio_product_license_compliance_export_view_json(self): + self.client.login(username=self.super_user.username, password="secret") + url = self.product1.get_export_license_compliance_url() + + license1 = make_license(self.dataspace, key="mit", short_name="MIT") + license2 = make_license(self.dataspace, key="apache-2.0", short_name="Apache 2.0") + make_product_package(self.product1, license_expression=license1.key) + make_product_package(self.product1, license_expression=f"{license1.key} AND {license2.key}") + + response = self.client.get(url + "?export=json") + self.assertEqual(200, response.status_code) + self.assertEqual("application/json", response["Content-Type"]) + self.assertIn("license_compliance_", response["Content-Disposition"]) + + data = json.loads(response.content) + self.assertEqual(2, len(data)) + # Ordered by package_count desc: MIT (2 packages) before Apache (1 package) + self.assertEqual("mit", data[0]["key"]) + self.assertEqual(2, data[0]["package_count"]) + self.assertEqual("apache-2.0", data[1]["key"]) + self.assertEqual(1, data[1]["package_count"]) + + def test_product_portfolio_product_license_compliance_export_view_xlsx(self): + self.client.login(username=self.super_user.username, password="secret") + url = self.product1.get_export_license_compliance_url() + + license1 = make_license(self.dataspace, key="mit", short_name="MIT") + make_product_package(self.product1, license_expression=license1.key) + response = self.client.get(url + "?export=xlsx") + + self.assertEqual(200, response.status_code) + expected_type = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + self.assertEqual(expected_type, response["Content-Type"]) + self.assertIn("license_compliance_", response["Content-Disposition"]) + self.assertIn(".xlsx", response["Content-Disposition"]) + + def test_product_portfolio_product_license_compliance_export_view_ods(self): + self.client.login(username=self.super_user.username, password="secret") + url = self.product1.get_export_license_compliance_url() + + license1 = make_license(self.dataspace, key="mit", short_name="MIT") + make_product_package(self.product1, license_expression=license1.key) + + response = self.client.get(url + "?export=ods") + self.assertEqual(200, response.status_code) + expected_type = "application/vnd.oasis.opendocument.spreadsheet" + self.assertEqual(expected_type, response["Content-Type"]) + self.assertIn("license_compliance_", response["Content-Disposition"]) + self.assertIn(".ods", response["Content-Disposition"]) + + def test_product_portfolio_product_license_compliance_export_view_yaml(self): + self.client.login(username=self.super_user.username, password="secret") + url = self.product1.get_export_license_compliance_url() + + license1 = make_license(self.dataspace, key="mit", short_name="MIT") + make_product_package(self.product1, license_expression=license1.key) + + response = self.client.get(url + "?export=yaml") + self.assertEqual(200, response.status_code) + self.assertEqual("application/x-yaml", response["Content-Type"]) + self.assertIn("license_compliance_", response["Content-Disposition"]) + self.assertIn(".yaml", response["Content-Disposition"]) + + content = response.content.decode() + self.assertIn("mit", content) + self.assertIn("package_count", content) + + def test_product_portfolio_product_license_compliance_export_view_respects_permissions(self): + self.client.login(username=self.basic_user.username, password="secret") + url = self.product1.get_export_license_compliance_url() + + license1 = make_license(self.dataspace, key="mit", short_name="MIT") + make_product_package(self.product1, license_expression=license1.key) + + # Without permission, the detail lookup should 404 + response = self.client.get(url + "?export=json") + self.assertEqual(404, response.status_code) + + # With permission, the export is returned + assign_perm("view_product", self.basic_user, self.product1) + response = self.client.get(url + "?export=json") + self.assertEqual(200, response.status_code) + data = json.loads(response.content) + self.assertEqual("mit", data[0]["key"]) + + def test_product_portfolio_product_security_compliance_export_view_csv(self): + self.client.login(username=self.super_user.username, password="secret") + url = self.product1.get_export_security_compliance_url() + + package = make_package(self.dataspace, filename="isolated") + vulnerability = make_vulnerability( + self.dataspace, + affecting=package, + aliases=["CVE-2024-42005", "GHSA-pv4p-cwwg-4rph", "PYSEC-2024-70"], + ) + make_product_package(self.product1, package=package) + + response = self.client.get(url + "?export=csv") + self.assertEqual(200, response.status_code) + self.assertEqual("text/csv", response["Content-Type"]) + self.assertIn("security_compliance_", response["Content-Disposition"]) + self.assertIn(".csv", response["Content-Disposition"]) + self.assertIn("product1_with_space", response["Content-Disposition"]) + + content = response.content.decode() + expected_header = ( + "Vulnerability ID,Aliases,Summary,Risk level,Risk score," + "Exploitability,Weighted severity,Affected packages,Fixed packages," + "Reference URL" + ) + self.assertIn(expected_header, content) + self.assertIn(vulnerability.vulnerability_id, content) + # Aliases must be flattened to a comma-joined string, not a Python list repr. + self.assertIn('"CVE-2024-42005, GHSA-pv4p-cwwg-4rph, PYSEC-2024-70"', content) + self.assertNotIn("['CVE-2024-42005'", content) + + def test_product_portfolio_product_security_compliance_export_view_json(self): + self.client.login(username=self.super_user.username, password="secret") + url = self.product1.get_export_security_compliance_url() + + package = make_package(self.dataspace, filename="isolated") + vulnerability = make_vulnerability( + self.dataspace, + affecting=package, + aliases=["CVE-2024-42005", "GHSA-pv4p-cwwg-4rph", "PYSEC-2024-70"], + ) + make_product_package(self.product1, package=package) + + response = self.client.get(url + "?export=json") + self.assertEqual(200, response.status_code) + self.assertEqual("application/json", response["Content-Type"]) + self.assertIn("security_compliance_", response["Content-Disposition"]) + + data = json.loads(response.content) + self.assertEqual(1, len(data)) + self.assertEqual(vulnerability.vulnerability_id, data[0]["vulnerability_id"]) + # Aliases stay a real list in JSON, not a comma-joined string. + self.assertEqual( + ["CVE-2024-42005", "GHSA-pv4p-cwwg-4rph", "PYSEC-2024-70"], + data[0]["aliases"], + ) + self.assertEqual(1, data[0]["affected_package_count"]) + + def test_product_portfolio_product_security_compliance_export_view_xlsx(self): + self.client.login(username=self.super_user.username, password="secret") + url = self.product1.get_export_security_compliance_url() + + make_vulnerability( + self.dataspace, + affecting=self.package1, + aliases=["CVE-2024-42005", "GHSA-pv4p-cwwg-4rph", "PYSEC-2024-70"], + ) + make_product_package(self.product1, package=self.package1) + + response = self.client.get(url + "?export=xlsx") + self.assertEqual(200, response.status_code) + expected_type = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + self.assertEqual(expected_type, response["Content-Type"]) + self.assertIn("security_compliance_", response["Content-Disposition"]) + self.assertIn(".xlsx", response["Content-Disposition"]) + + def test_product_portfolio_product_security_compliance_export_view_ods(self): + self.client.login(username=self.super_user.username, password="secret") + url = self.product1.get_export_security_compliance_url() + + make_vulnerability( + self.dataspace, + affecting=self.package1, + aliases=["CVE-2024-42005", "GHSA-pv4p-cwwg-4rph", "PYSEC-2024-70"], + ) + make_product_package(self.product1, package=self.package1) + + response = self.client.get(url + "?export=ods") + self.assertEqual(200, response.status_code) + expected_type = "application/vnd.oasis.opendocument.spreadsheet" + self.assertEqual(expected_type, response["Content-Type"]) + self.assertIn("security_compliance_", response["Content-Disposition"]) + self.assertIn(".ods", response["Content-Disposition"]) + + def test_product_portfolio_product_security_compliance_export_view_yaml(self): + self.client.login(username=self.super_user.username, password="secret") + url = self.product1.get_export_security_compliance_url() + + vulnerability = make_vulnerability( + self.dataspace, + affecting=self.package1, + aliases=["CVE-2024-42005", "GHSA-pv4p-cwwg-4rph", "PYSEC-2024-70"], + ) + make_product_package(self.product1, package=self.package1) + + response = self.client.get(url + "?export=yaml") + self.assertEqual(200, response.status_code) + self.assertEqual("application/x-yaml", response["Content-Type"]) + self.assertIn("security_compliance_", response["Content-Disposition"]) + self.assertIn(".yaml", response["Content-Disposition"]) + + content = response.content.decode() + self.assertIn(vulnerability.vulnerability_id, content) + self.assertIn("vulnerability_id", content) + + def test_product_portfolio_product_security_compliance_export_view_respects_permissions(self): + self.client.login(username=self.basic_user.username, password="secret") + url = self.product1.get_export_security_compliance_url() + + package = make_package(self.dataspace, filename="isolated") + vulnerability = make_vulnerability( + self.dataspace, + affecting=package, + aliases=["CVE-2024-42005", "GHSA-pv4p-cwwg-4rph", "PYSEC-2024-70"], + ) + make_product_package(self.product1, package=package) + + # Without permission, the detail lookup should 404 + response = self.client.get(url + "?export=json") + self.assertEqual(404, response.status_code) + + # With permission, the export is returned + assign_perm("view_product", self.basic_user, self.product1) + response = self.client.get(url + "?export=json") + self.assertEqual(200, response.status_code) + data = json.loads(response.content) + vulnerability_ids = [entry["vulnerability_id"] for entry in data] + self.assertIn(vulnerability.vulnerability_id, vulnerability_ids) diff --git a/product_portfolio/urls.py b/product_portfolio/urls.py index 70425ee4..f4115bdd 100644 --- a/product_portfolio/urls.py +++ b/product_portfolio/urls.py @@ -9,6 +9,7 @@ from django.urls import path from product_portfolio.views import AttributionView +from product_portfolio.views import ComplianceDashboardView from product_portfolio.views import ImportManifestsView from product_portfolio.views import LoadSBOMsView from product_portfolio.views import ManageComponentGridView @@ -18,10 +19,14 @@ from product_portfolio.views import ProductDetailsView from product_portfolio.views import ProductExportCSAFDocumentView from product_portfolio.views import ProductExportCycloneDXBOMView +from product_portfolio.views import ProductExportOpenVEXView from product_portfolio.views import ProductExportSPDXDocumentView +from product_portfolio.views import ProductLicenseComplianceExportView from product_portfolio.views import ProductListView +from product_portfolio.views import ProductSecurityComplianceExportView from product_portfolio.views import ProductSendAboutFilesView from product_portfolio.views import ProductTabCodebaseView +from product_portfolio.views import ProductTabComplianceView from product_portfolio.views import ProductTabDependenciesView from product_portfolio.views import ProductTabImportsView from product_portfolio.views import ProductTabInventoryView @@ -31,12 +36,14 @@ from product_portfolio.views import PullProjectDataFromScanCodeIOView from product_portfolio.views import add_customcomponent_ajax_view from product_portfolio.views import check_package_version_ajax_view +from product_portfolio.views import delete_scan_htmx_view from product_portfolio.views import edit_productrelation_ajax_view from product_portfolio.views import import_from_scan_view from product_portfolio.views import import_packages_from_scancodeio_view from product_portfolio.views import improve_packages_from_purldb_view from product_portfolio.views import license_summary_view from product_portfolio.views import scan_all_packages_view +from product_portfolio.views import scancodeio_project_download_input_view from product_portfolio.views import scancodeio_project_status_view from product_portfolio.views import vulnerability_analysis_form_view @@ -61,6 +68,11 @@ def product_path(path_segment, view): urlpatterns = [ + path( + "compliance_dashboard/", + ComplianceDashboardView.as_view(), + name="compliance_dashboard", + ), path( "import_packages_from_scancodeio//", import_packages_from_scancodeio_view, @@ -71,6 +83,11 @@ def product_path(path_segment, view): scancodeio_project_status_view, name="scancodeio_project_status", ), + path( + "scancodeio_project_download_input//", + scancodeio_project_download_input_view, + name="scancodeio_project_download_input", + ), path( "compare///", ProductTreeComparisonView.as_view(), @@ -86,6 +103,11 @@ def product_path(path_segment, view): vulnerability_analysis_form_view, name="vulnerability_analysis_form", ), + path( + "scans///delete/", + delete_scan_htmx_view, + name="scan_delete_htmx", + ), *product_path("add_customcomponent_ajax", add_customcomponent_ajax_view), *product_path("vulnerability_analysis_form", vulnerability_analysis_form_view), *product_path("scan_all_packages", scan_all_packages_view), @@ -94,6 +116,9 @@ def product_path(path_segment, view): *product_path("export_spdx", ProductExportSPDXDocumentView.as_view()), *product_path("export_cyclonedx", ProductExportCycloneDXBOMView.as_view()), *product_path("export_csaf", ProductExportCSAFDocumentView.as_view()), + *product_path("export_openvex", ProductExportOpenVEXView.as_view()), + *product_path("export_license_compliance", ProductLicenseComplianceExportView.as_view()), + *product_path("export_security_compliance", ProductSecurityComplianceExportView.as_view()), *product_path("attribution", AttributionView.as_view()), *product_path("change", ProductUpdateView.as_view()), *product_path("delete", ProductDeleteView.as_view()), @@ -109,6 +134,7 @@ def product_path(path_segment, view): *product_path("tab_vulnerabilities", ProductTabVulnerabilitiesView.as_view()), *product_path("tab_imports", ProductTabImportsView.as_view()), *product_path("tab_inventory", ProductTabInventoryView.as_view()), + *product_path("tab_compliance", ProductTabComplianceView.as_view()), *product_path("pull_project_data", PullProjectDataFromScanCodeIOView.as_view()), path( "///", diff --git a/product_portfolio/views.py b/product_portfolio/views.py index 8c05d393..cc7d0ece 100644 --- a/product_portfolio/views.py +++ b/product_portfolio/views.py @@ -7,6 +7,7 @@ # import csv +import io import json from collections import OrderedDict from collections import defaultdict @@ -24,13 +25,21 @@ from django.core.exceptions import ValidationError from django.core.paginator import Paginator from django.db import transaction +from django.db.models import Case +from django.db.models import CharField from django.db.models import Count from django.db.models import Exists +from django.db.models import F from django.db.models import OuterRef from django.db.models import Prefetch +from django.db.models import Q from django.db.models import Subquery +from django.db.models import Sum +from django.db.models import Value +from django.db.models import When from django.db.models.functions import Lower from django.forms import modelformset_factory +from django.http import FileResponse from django.http import Http404 from django.http import HttpResponse from django.http import JsonResponse @@ -42,21 +51,21 @@ from django.urls import reverse from django.utils.decorators import method_decorator from django.utils.html import format_html +from django.utils.html import mark_safe from django.utils.translation import gettext_lazy as _ from django.views.decorators.csrf import csrf_exempt +from django.views.decorators.http import require_http_methods from django.views.decorators.http import require_POST from django.views.generic import DetailView from django.views.generic import FormView from django.views.generic import TemplateView +from django.views.generic.detail import BaseDetailView +import odfdo +import saneyaml from crispy_forms.utils import render_crispy_form from guardian.shortcuts import get_perms as guardian_get_perms from openpyxl import Workbook -from openpyxl.styles import Alignment -from openpyxl.styles import Border -from openpyxl.styles import Font -from openpyxl.styles import NamedStyle -from openpyxl.styles import Side from component_catalog.forms import ComponentAjaxForm from component_catalog.license_expression_dje import build_licensing @@ -71,7 +80,7 @@ from dejacode_toolkit.scancodeio import get_scan_results_as_file_url from dejacode_toolkit.utils import sha1 from dejacode_toolkit.vulnerablecode import VulnerableCode -from dje import tasks +from dje import outputs from dje.client_data import add_client_data from dje.filters import BooleanChoiceFilter from dje.filters import HasCountFilter @@ -83,6 +92,7 @@ from dje.utils import get_object_compare_diff from dje.utils import group_by_simple from dje.utils import is_uuid4 +from dje.utils import style_xlsx_worksheet from dje.views import DataspacedCreateView from dje.views import DataspacedDeleteView from dje.views import DataspacedFilterView @@ -91,12 +101,13 @@ from dje.views import DataspaceScopeMixin from dje.views import ExportCSAFDocumentView from dje.views import ExportCycloneDXBOMView +from dje.views import ExportOpenVEXView from dje.views import ExportSPDXDocumentView from dje.views import GetDataspacedObjectMixin from dje.views import Header from dje.views import LicenseDataForBuilderMixin from dje.views import ObjectDetailsView -from dje.views import PreviousNextPaginationMixin +from dje.views import PaginationMixin from dje.views import SendAboutFilesView from dje.views import TabContentView from dje.views import TabField @@ -124,6 +135,7 @@ from product_portfolio.forms import ProductPackageForm from product_portfolio.forms import ProductPackageInlineForm from product_portfolio.forms import PullProjectDataForm +from product_portfolio.forms import ScanAllPackagesForm from product_portfolio.forms import TableInlineFormSetHelper from product_portfolio.models import RELATION_LICENSE_EXPRESSION_HELP_TEXT from product_portfolio.models import CodebaseResource @@ -133,10 +145,13 @@ from product_portfolio.models import ProductPackage from product_portfolio.models import ProductRelationshipMixin from product_portfolio.models import ScanCodeProject +from product_portfolio.tasks import improve_packages_from_purldb_task +from product_portfolio.tasks import pull_project_data_from_scancodeio_task from vulnerabilities.forms import VulnerabilityAnalysisForm from vulnerabilities.models import AffectedByVulnerabilityMixin from vulnerabilities.models import Vulnerability from vulnerabilities.models import VulnerabilityAnalysis +from vulnerabilities.models import get_risk_level class BaseProductViewMixin: @@ -157,8 +172,7 @@ class ProductListView( model = Product filterset_class = ProductFilterSet template_name = "product_portfolio/product_list.html" - template_list_table = "product_portfolio/includes/product_list_table.html" - paginate_by = 50 + template_list_table = "product_portfolio/tables/product_list_table.html" put_results_in_session = False group_name_version = True table_headers = ( @@ -203,7 +217,7 @@ def get_queryset(self): ) .annotate( productinventoryitem_count=Count("productinventoryitem", distinct=True), - is_vulnerable=Exists(vulnerable_productpackage_qs), + has_vulnerable_packages=Exists(vulnerable_productpackage_qs), ) .order_by( "name", @@ -269,6 +283,11 @@ class ProductDetailsView( "dataspace", ], }, + "compliance": { + "fields": [ + "packages", + ], + }, "inventory": { "fields": [ "components", @@ -489,6 +508,27 @@ def tab_hierarchy(self): return {"fields": [(None, context, None, template)]} + def tab_compliance(self): + if not self.has_packages: + return + + template = "tabs/tab_async_loader.html" + + # Pass the current request query context to the async request + tab_view_url = self.object.get_url("tab_compliance") + if full_query_string := self.request.META["QUERY_STRING"]: + tab_view_url += f"?{full_query_string}" + + tab_context = { + "tab_view_url": tab_view_url, + "tab_object_name": "compliance", + } + + return { + "label": "Compliance", + "fields": [(None, tab_context, None, template)], + } + def tab_inventory(self): productcomponents_count = self.object.productcomponents.count() productpackages_count = self.object.productpackages.count() @@ -510,7 +550,7 @@ def tab_inventory(self): } return { - "label": format_html(label), + "label": mark_safe(label), "fields": [(None, tab_context, None, template)], } @@ -533,11 +573,14 @@ def tab_dependencies(self): } return { - "label": format_html(label), + "label": mark_safe(label), "fields": [(None, tab_context, None, template)], } def tab_vulnerabilities(self): + if not self.has_packages: + return + product = self.object dataspace = product.dataspace vulnerablecode = VulnerableCode(dataspace) @@ -561,7 +604,7 @@ def tab_vulnerabilities(self): if not vulnerability_count and risk_threshold is None: label = 'Vulnerabilities 0' return { - "label": format_html(label), + "label": mark_safe(label), "fields": [], "disabled": True, "tooltip": "No vulnerabilities found in this Product", @@ -591,7 +634,7 @@ def tab_vulnerabilities(self): } return { - "label": format_html(label), + "label": mark_safe(label), "fields": [(None, tab_context, None, template)], } @@ -614,7 +657,7 @@ def tab_codebase(self): } return { - "label": format_html(label), + "label": mark_safe(label), "fields": [(None, tab_context, None, template)], } @@ -640,61 +683,61 @@ def tab_imports(self): } return { - "label": format_html(label), + "label": mark_safe(label), "fields": [(None, tab_context, None, template)], } def get_context_data(self, **kwargs): + product = self.object user = self.request.user dataspace = user.dataspace + self.has_packages = self.object.productpackages.exists() # This behavior does not works well in the context of getting informed about # tasks completion on the Product. if user.is_authenticated: - self.object.mark_all_notifications_as_read(user) + product.mark_all_notifications_as_read(user) context = super().get_context_data(**kwargs) - - context["has_change_codebaseresource_permission"] = user.has_perm( - "product_portfolio.change_codebaseresource" - ) - context["filter_productcomponent"] = self.filter_productcomponent context["filter_productpackage"] = self.filter_productpackage # The reference data label and help does not make sense in the Product context context["is_reference_data"] = None - perms = guardian_get_perms(user, self.object) + perms = guardian_get_perms(user, product) context["has_change_permission"] = "change_product" in perms context["has_delete_permission"] = "delete_product" in perms - context["has_edit_productpackage"] = all( - [ - user.has_perm("product_portfolio.change_productpackage"), - context["has_change_permission"], - ] - ) - context["has_delete_productpackage"] = user.has_perm( - "product_portfolio.delete_productpackage" - ) - - context["has_add_productcomponent"] = all( - [ - user.has_perm("product_portfolio.add_productcomponent"), - context["has_change_permission"], - ] - ) - context["has_edit_productcomponent"] = all( - [ - user.has_perm("product_portfolio.change_productcomponent"), - context["has_change_permission"], - ] - ) - context["has_delete_productcomponent"] = user.has_perm( - "product_portfolio.delete_productcomponent" - ) + if not product.is_locked: + context["has_edit_productpackage"] = all( + [ + user.has_perm("product_portfolio.change_productpackage"), + context["has_change_permission"], + ] + ) + context["has_delete_productpackage"] = user.has_perm( + "product_portfolio.delete_productpackage" + ) + context["has_add_productcomponent"] = all( + [ + user.has_perm("product_portfolio.add_productcomponent"), + context["has_change_permission"], + ] + ) + context["has_edit_productcomponent"] = all( + [ + user.has_perm("product_portfolio.change_productcomponent"), + context["has_change_permission"], + ] + ) + context["has_delete_productcomponent"] = user.has_perm( + "product_portfolio.delete_productcomponent" + ) + context["has_change_codebaseresource_permission"] = user.has_perm( + "product_portfolio.change_codebaseresource" + ) - if context["has_edit_productpackage"] or context["has_edit_productcomponent"]: + if context.get("has_edit_productpackage") or context.get("has_edit_productcomponent"): all_licenses = License.objects.scope(dataspace).filter(is_active=True) add_client_data(self.request, license_data=all_licenses.data_for_expression_builder()) @@ -702,9 +745,9 @@ def get_context_data(self, **kwargs): include_scancodeio_features = all( [ scancodeio.is_configured(), - user.is_superuser, dataspace.enable_package_scanning, context["is_user_dataspace"], + context["has_change_permission"], ] ) context["has_scan_all_packages"] = include_scancodeio_features @@ -712,6 +755,7 @@ def get_context_data(self, **kwargs): if include_scancodeio_features: context["pull_project_data_form"] = PullProjectDataForm() context["display_scan_features"] = True + context["scan_all_packages_form"] = ScanAllPackagesForm() context["purldb_enabled"] = all( [ @@ -727,7 +771,7 @@ def get_context_data(self, **kwargs): class ProductTabInventoryView( LoginRequiredMixin, BaseProductViewMixin, - PreviousNextPaginationMixin, + PaginationMixin, TabContentView, ): template_name = "product_portfolio/tabs/tab_inventory.html" @@ -738,12 +782,13 @@ class ProductTabInventoryView( def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) + product = self.object user = self.request.user dataspace = user.dataspace - context["inventory_count"] = self.object.productinventoryitem_set.count() + context["inventory_count"] = product.productinventoryitem_set.count() license_qs = License.objects.select_related("usage_policy") - declared_dependencies_qs = ProductDependency.objects.product(self.object) + declared_dependencies_qs = ProductDependency.objects.product(product) package_qs = ( Package.objects.select_related( "dataspace", @@ -759,7 +804,7 @@ def get_context_data(self, **kwargs): ).with_vulnerability_count() productpackage_qs = ( - self.object.productpackages.select_related( + product.productpackages.select_related( "review_status", "purpose", ) @@ -780,13 +825,13 @@ def get_context_data(self, **kwargs): filter_productpackage = ProductPackageFilterSet( self.request.GET, queryset=productpackage_qs, - dataspace=self.object.dataspace, + dataspace=product.dataspace, prefix=self.tab_id, anchor="#inventory", ) productcomponent_qs = ( - self.object.productcomponents.select_related( + product.productcomponents.select_related( "review_status", "purpose", ) @@ -808,7 +853,7 @@ def get_context_data(self, **kwargs): filter_productcomponent = ProductComponentFilterSet( self.request.GET, queryset=productcomponent_qs, - dataspace=self.object.dataspace, + dataspace=product.dataspace, prefix=self.tab_id, anchor="#inventory", ) @@ -875,26 +920,27 @@ def get_context_data(self, **kwargs): } ) - perms = guardian_get_perms(user, self.object) - has_product_change_permission = "change_product" in perms - context["has_edit_productcomponent"] = all( - [ - has_product_change_permission, - user.has_perm("product_portfolio.change_productcomponent"), - ] - ) - context["has_edit_productpackage"] = all( - [ - has_product_change_permission, - user.has_perm("product_portfolio.change_productpackage"), - ] - ) - context["has_delete_productpackage"] = user.has_perm( - "product_portfolio.delete_productpackage" - ) - context["has_delete_productcomponent"] = user.has_perm( - "product_portfolio.delete_productcomponent" - ) + if not product.is_locked: + perms = guardian_get_perms(user, product) + has_product_change_permission = "change_product" in perms + context["has_edit_productcomponent"] = all( + [ + has_product_change_permission, + user.has_perm("product_portfolio.change_productcomponent"), + ] + ) + context["has_edit_productpackage"] = all( + [ + has_product_change_permission, + user.has_perm("product_portfolio.change_productpackage"), + ] + ) + context["has_delete_productpackage"] = user.has_perm( + "product_portfolio.delete_productpackage" + ) + context["has_delete_productcomponent"] = user.has_perm( + "product_portfolio.delete_productcomponent" + ) if page_obj: previous_url, next_url = self.get_previous_next(page_obj) @@ -951,9 +997,13 @@ def inject_scan_data(scancodeio, feature_grouped, dataspace_uuid): for productpackage in productpackages: if not isinstance(productpackage, ProductPackage): continue - scan = scans_by_uri.get(productpackage.package.download_url) + package = productpackage.package + scan = scans_by_uri.get(package.download_url) if scan: scan["download_result_url"] = get_scan_results_as_file_url(scan) + scan["delete_url"] = reverse( + "product_portfolio:scan_delete_htmx", args=[scan.get("uuid"), package.uuid] + ) productpackage.scan = scan injected_productpackages.append(productpackage) @@ -966,7 +1016,7 @@ def inject_scan_data(scancodeio, feature_grouped, dataspace_uuid): class ProductTabCodebaseView( LoginRequiredMixin, BaseProductViewMixin, - PreviousNextPaginationMixin, + PaginationMixin, TabContentView, ): template_name = "product_portfolio/tabs/tab_codebase.html" @@ -1047,7 +1097,7 @@ def has_any_values(field_name): class ProductTabDependenciesView( LoginRequiredMixin, BaseProductViewMixin, - PreviousNextPaginationMixin, + PaginationMixin, TableHeaderMixin, TabContentView, ): @@ -1126,7 +1176,7 @@ def get_context_data(self, **kwargs): class ProductTabVulnerabilitiesView( LoginRequiredMixin, BaseProductViewMixin, - PreviousNextPaginationMixin, + PaginationMixin, TableHeaderMixin, TabContentView, ): @@ -1311,7 +1361,7 @@ def synchronize(self, scancodeio, project): if run_status != project.status: if run_status == "success": transaction.on_commit( - lambda: tasks.pull_project_data_from_scancodeio.delay( + lambda: pull_project_data_from_scancodeio_task.delay( scancodeproject_uuid=project.uuid, ) ) @@ -1647,16 +1697,7 @@ def get_relation_data(relation, diff, is_left): ws.append(row) # Styling - header = NamedStyle(name="header") - header.font = Font(bold=True) - header.border = Border(bottom=Side(border_style="thin")) - header.alignment = Alignment(horizontal="center", vertical="center") - header_row = ws[1] - for cell in header_row: - cell.style = header - - # Freeze first header row - ws.freeze_panes = "A2" + style_xlsx_worksheet(ws) # Columns width ws.column_dimensions["B"].width = 40 @@ -1948,6 +1989,11 @@ class ProductExportCSAFDocumentView(BaseProductViewMixin, ExportCSAFDocumentView pass +class ProductExportOpenVEXView(BaseProductViewMixin, ExportOpenVEXView): + pass + + +@require_POST @login_required def scan_all_packages_view(request, dataspace, name, version=""): user = request.user @@ -1956,7 +2002,6 @@ def scan_all_packages_view(request, dataspace, name, version=""): scancodeio = ScanCodeIO(user_dataspace) conditions = [ scancodeio.is_configured(), - user.is_superuser, user_dataspace.enable_package_scanning, user_dataspace.name == dataspace, ] @@ -1964,13 +2009,18 @@ def scan_all_packages_view(request, dataspace, name, version=""): if not all(conditions): raise Http404 - guarded_qs = Product.objects.get_queryset(user) + guarded_qs = Product.objects.get_queryset(user, perms="change_product") product = get_object_or_404(guarded_qs, name=unquote_plus(name), version=unquote_plus(version)) - if not product.all_packages: raise Http404("No packages available for this product.") - transaction.on_commit(lambda: product.scan_all_packages_task(user)) + scan_all_packages_form = ScanAllPackagesForm(data=request.POST) + if not scan_all_packages_form.is_valid(): + raise Http404 + + infer_download_urls = scan_all_packages_form.cleaned_data.get("infer_download_urls") + + transaction.on_commit(lambda: product.scan_all_packages_task(user, infer_download_urls)) scan_list_url = reverse("component_catalog:scan_list") scancode_msg = format_html( @@ -2017,9 +2067,9 @@ def import_from_scan_view(request, dataspace, name, version=""): msg = "Imported from Scan:" for key, value in created_counts.items(): msg += f"
        • {value} {key}" - messages.success(request, format_html(msg)) + messages.success(request, mark_safe(msg)) if warnings: - messages.warning(request, format_html("
        ".join(warnings))) + messages.warning(request, mark_safe("
        ".join(warnings))) return redirect(f"{product.get_absolute_url()}#imports") else: form = form_class(request.user) @@ -2451,7 +2501,7 @@ def import_packages_from_scancodeio_view(request, key): get_object_or_404(product_qs, id=scancode_project.product_id) transaction.on_commit( - lambda: tasks.pull_project_data_from_scancodeio.delay( + lambda: pull_project_data_from_scancodeio_task.delay( scancodeproject_uuid=scancode_project.uuid, ) ) @@ -2486,6 +2536,18 @@ def scancodeio_project_status_view(request, scancodeproject_uuid): return TemplateResponse(request, template, context) +@login_required +def scancodeio_project_download_input_view(request, scancodeproject_uuid): + secured_qs = ScanCodeProject.objects.product_secured(user=request.user) + scancode_project = get_object_or_404(secured_qs, uuid=scancodeproject_uuid) + input_file = scancode_project.input_file + + if not input_file or not input_file.storage.exists(input_file.name): + raise Http404 + + return FileResponse(input_file.open("rb"), as_attachment=True) + + @login_required def improve_packages_from_purldb_view(request, dataspace, name, version=""): user = request.user @@ -2517,7 +2579,7 @@ def improve_packages_from_purldb_view(request, dataspace, name, version=""): messages.error(request, "Improve Packages already in progress...") else: transaction.on_commit( - lambda: tasks.improve_packages_from_purldb( + lambda: improve_packages_from_purldb_task( product_uuid=product.uuid, user_uuid=user.uuid, ) @@ -2588,3 +2650,474 @@ def vulnerability_analysis_form_view(request, productpackage_uuid, vulnerability rendered_form = render_crispy_form(form, context=csrf(request)) return HttpResponse(rendered_form) + + +@login_required +@csrf_exempt +@require_http_methods(["DELETE"]) +def delete_scan_htmx_view(request, project_uuid, package_uuid): + dataspace = request.user.dataspace + package = get_object_or_404(Package, uuid=package_uuid, dataspace=dataspace) + + if not dataspace.enable_package_scanning: + raise Http404 + + scancodeio = ScanCodeIO(dataspace) + scan_list = scancodeio.fetch_scan_list(uuid=str(project_uuid)) + + if not scan_list or scan_list.get("count") != 1: + raise Http404("Scan not found.") + + scan_detail_url = scancodeio.get_scan_detail_url(project_uuid) + deleted = scancodeio.delete_scan(scan_detail_url) + + if not deleted: + raise Http404("Scan could not be deleted.") + + # Return the rendered scan action cell content. + template = "product_portfolio/tables/scan_action_cell.html" + context = { + "package": package, + "user": request.user, + } + return render(request, template, context) + + +class ProductTabComplianceView( + LoginRequiredMixin, + BaseProductViewMixin, + TabContentView, +): + template_name = "product_portfolio/tabs/tab_compliance.html" + tab_id = "compliance" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + product = self.object + productpackages = product.productpackages.all() + licenses = License.objects.filter(productpackage__in=productpackages) + + context.update( + { + **self.get_package_compliance_context(productpackages), + **self.get_license_compliance_context(licenses), + **self.get_security_compliance_context(product), + } + ) + + return context + + @staticmethod + def get_package_compliance_context(productpackages): + # "Total packages" card: alert at the Package level. + total_packages = productpackages.count() + package_issues_count = productpackages.filter( + package__usage_policy__compliance_alert__in=["warning", "error"] + ).count() + + # "License compliance" card: alert at the ProductPackage license level. + packages_with_license_issues = ( + productpackages.filter( + licenses__usage_policy__compliance_alert__in=["warning", "error"] + ) + .distinct() + .count() + ) + license_compliance_pct = ( + round(((total_packages - packages_with_license_issues) / total_packages) * 100) + if total_packages + else 100 + ) + + # "License coverage" card: missing license at the ProductPackage level. + package_without_license_count = productpackages.filter(license_expression="").count() + license_coverage_pct = ( + round(((total_packages - package_without_license_count) / total_packages) * 100) + if total_packages + else 100 + ) + + return { + "total_packages": total_packages, + "package_issues_count": package_issues_count, + "packages_with_license_issues": packages_with_license_issues, + "license_compliance_pct": license_compliance_pct, + "license_coverage_pct": license_coverage_pct, + "package_without_license_count": package_without_license_count, + } + + @staticmethod + def get_license_compliance_context(licenses, distribution_limit=10): + license_distribution = list( + licenses.values("key", "short_name", "spdx_license_key") + .annotate( + package_count=Count("productpackage"), + compliance_alert=F("usage_policy__compliance_alert"), + ) + .order_by("-package_count") + ) + license_error_count = sum( + 1 for entry in license_distribution if entry["compliance_alert"] == "error" + ) + license_warning_count = sum( + 1 for entry in license_distribution if entry["compliance_alert"] == "warning" + ) + + return { + "license_issues_count": license_error_count + license_warning_count, + "license_error_count": license_error_count, + "license_warning_count": license_warning_count, + "license_distribution": license_distribution[:distribution_limit], + "license_distribution_limit": distribution_limit, + "remaining_license_count": max(0, len(license_distribution) - distribution_limit), + } + + @staticmethod + def get_security_compliance_context(product, display_limit=10): + risk_threshold = product.get_vulnerabilities_risk_threshold() + risk_threshold_label = get_risk_level(risk_threshold) + + all_vulnerabilities = product.get_vulnerability_qs(risk_threshold=None) + vulnerability_count = all_vulnerabilities.count() + + if risk_threshold is not None: + above_threshold_count = all_vulnerabilities.filter( + risk_score__gte=risk_threshold + ).count() + else: + above_threshold_count = vulnerability_count + + all_vulnerabilities_ordered = all_vulnerabilities.order_by_risk() + + max_vulnerability_severity = None + first = all_vulnerabilities_ordered.first() + if first: + max_vulnerability_severity = first.risk_level + + severity_counts = all_vulnerabilities.aggregate( + critical_count=Count("id", filter=Q(risk_level="critical")), + high_count=Count("id", filter=Q(risk_level="high")), + medium_count=Count("id", filter=Q(risk_level="medium")), + low_count=Count("id", filter=Q(risk_level="low")), + ) + + return { + "risk_threshold_number": risk_threshold, + "risk_threshold": risk_threshold_label, + "max_vulnerability_severity": max_vulnerability_severity, + "vulnerability_count": vulnerability_count, + "above_threshold_count": above_threshold_count, + "vulnerabilities": all_vulnerabilities_ordered[:display_limit], + **severity_counts, + } + + +class ExportComplianceMixin: + """Mixin for views that support CSV, XLSX, and JSON export.""" + + export_filename = "export" + export_fields = {} + + def get(self, request, *args, **kwargs): + export_format = request.GET.get("export") + if export_format in ("csv", "xlsx", "json", "ods", "yaml"): + return self.export(export_format) + return super().get(request, *args, **kwargs) + + def get_export_headers(self): + return list(self.export_fields.values()) + + def get_export_fields(self): + return list(self.export_fields.keys()) + + def get_export_queryset(self): + return self.get_queryset() + + def get_export_rows(self): + return self.get_export_queryset().values_list(*self.get_export_fields()) + + def build_export_filename(self, extension): + instance = getattr(self, "object", None) + if instance: + dataspace = instance.dataspace + else: + dataspace = self.dataspace + + return outputs.get_export_filename( + dataspace=dataspace, + report_type=self.export_filename, + extension=extension, + instance=instance, + ) + + def get_content_disposition(self, extension): + return f'attachment; filename="{self.build_export_filename(extension)}"' + + def export(self, export_format): + if export_format == "csv": + return self.export_csv() + if export_format == "xlsx": + return self.export_xlsx() + if export_format == "ods": + return self.export_ods() + if export_format == "yaml": + return self.export_yaml() + return self.export_json() + + @staticmethod + def normalize_cell_value(value): + """Convert list values to comma-joined strings for spreadsheet cells.""" + if isinstance(value, list): + return ", ".join(str(item) for item in value) + return value + + def export_csv(self): + response = HttpResponse(content_type="text/csv") + response["Content-Disposition"] = self.get_content_disposition("csv") + writer = csv.writer(response) + writer.writerow(self.get_export_headers()) + writer.writerows( + [self.normalize_cell_value(value) for value in row] for row in self.get_export_rows() + ) + return response + + def export_xlsx(self): + workbook = Workbook() + worksheet = workbook.active + worksheet.title = self.export_filename.replace("_", " ").title() + + headers = self.get_export_headers() + worksheet.append(headers) + + for row in self.get_export_rows(): + worksheet.append([self.normalize_cell_value(value) for value in row]) + + style_xlsx_worksheet(worksheet, headers) + + response = HttpResponse( + content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + ) + response["Content-Disposition"] = self.get_content_disposition("xlsx") + workbook.save(response) + return response + + def export_json(self): + data = list(self.get_export_queryset().values(*self.get_export_fields())) + response = HttpResponse( + json.dumps(data, indent=2, default=str), + content_type="application/json", + ) + response["Content-Disposition"] = self.get_content_disposition("json") + return response + + def export_ods(self): + title = self.export_filename.replace("_", " ").title() + table = odfdo.Table(title) + + for row_data in [self.get_export_headers()] + list(self.get_export_rows()): + row = odfdo.Row() + for value in row_data: + normalized = self.normalize_cell_value(value) + cell_value = str(normalized) if normalized is not None else "" + row.append(odfdo.Cell(cell_value, cell_type="string")) + table.append(row) + + document = odfdo.Document("spreadsheet") + document.body.clear() + document.body.append(table) + + file_output = io.BytesIO() + document.save(file_output) + + response = HttpResponse( + file_output.getvalue(), + content_type="application/vnd.oasis.opendocument.spreadsheet", + ) + response["Content-Disposition"] = self.get_content_disposition("ods") + return response + + def export_yaml(self): + fields = self.get_export_fields() + # Round-trip through JSON to convert Decimal and other non-serializable types + data = json.loads(json.dumps(list(self.get_export_queryset().values(*fields)), default=str)) + response = HttpResponse( + saneyaml.dump(data), + content_type="application/x-yaml", + ) + response["Content-Disposition"] = self.get_content_disposition("yaml") + return response + + +class ComplianceDashboardView(LoginRequiredMixin, ExportComplianceMixin, DataspacedFilterView): + """Compliance control center: overview of all products.""" + + template_name = "product_portfolio/compliance_dashboard.html" + model = Product + filterset_class = ProductFilterSet + paginate_by = settings.DEJACODE_PAGINATE_BY.get("compliance", 50) + export_filename = "compliance_dashboard" + export_fields = { + "name": "Product", + "version": "Version", + "package_count": "Packages", + "license_error_count": "License errors", + "license_warning_count": "License warnings", + "max_risk_level": "Max risk level", + "risk_threshold": "Risk threshold", + "critical_count": "Critical", + "high_count": "High", + "medium_count": "Medium", + "low_count": "Low", + "vulnerability_count": "Total vulnerabilities", + } + + def get_queryset(self): + base_qs = Product.objects.get_queryset( + user=self.request.user, + perms="view_product", + include_inactive=False, + exclude_locked=True, + ) + + return ( + base_qs.with_risk_threshold() + .with_vulnerability_counts() + .with_license_compliance_counts() + .annotate( + package_count=Count("productpackages", distinct=True), + max_risk_level=Case( + When(max_risk_score__gte=8.0, then=Value("critical")), + When(max_risk_score__gte=6.0, then=Value("high")), + When(max_risk_score__gte=3.0, then=Value("medium")), + When(max_risk_score__gte=0.1, then=Value("low")), + default=Value(""), + output_field=CharField(max_length=8), + ), + ) + .order_by( + F("max_risk_score").desc(nulls_last=True), + "-license_error_count", + "-license_warning_count", + "name", + "-version", + ) + ) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + products = self.object_list + total_products = products.count() + + products_with_issues = products.filter( + Q(license_error_count__gt=0) + | Q(license_warning_count__gt=0) + | Q(critical_count__gt=0) + | Q(high_count__gt=0) + ).count() + + products_with_license_issues = products.filter( + Q(license_error_count__gt=0) | Q(license_warning_count__gt=0) + ).count() + + products_with_critical_or_high = products.filter( + Q(critical_count__gt=0) | Q(high_count__gt=0) + ).count() + + totals = products.aggregate( + total_vulnerabilities=Sum("vulnerability_count"), + total_critical=Sum("critical_count"), + total_high=Sum("high_count"), + total_medium=Sum("medium_count"), + total_low=Sum("low_count"), + ) + + context.update( + { + "total_products": total_products, + "products_with_issues": products_with_issues, + "products_with_license_issues": products_with_license_issues, + "products_with_critical_or_high": products_with_critical_or_high, + "total_vulnerabilities": totals["total_vulnerabilities"] or 0, + "total_critical": totals["total_critical"] or 0, + "total_high": totals["total_high"] or 0, + "total_medium": totals["total_medium"] or 0, + "total_low": totals["total_low"] or 0, + } + ) + + return context + + +class ProductLicenseComplianceExportView( + LoginRequiredMixin, + ExportComplianceMixin, + BaseProductViewMixin, + DataspaceScopeMixin, + GetDataspacedObjectMixin, + BaseDetailView, +): + """Export license compliance data for a single product.""" + + export_filename = "license_compliance" + export_fields = { + "spdx_license_key": "SPDX license key", + "short_name": "Short name", + "key": "Key", + "package_count": "Packages", + "compliance_alert": "Compliance alert", + } + + def get(self, request, *args, **kwargs): + self.object = self.get_object() + return super().get(request, *args, **kwargs) + + def get_export_queryset(self): + productpackages = self.object.productpackages.all() + licenses = License.objects.filter(productpackage__in=productpackages) + return licenses.annotate( + package_count=Count("productpackage"), + compliance_alert=F("usage_policy__compliance_alert"), + ).order_by("-package_count") + + +class ProductSecurityComplianceExportView( + LoginRequiredMixin, + ExportComplianceMixin, + BaseProductViewMixin, + DataspaceScopeMixin, + GetDataspacedObjectMixin, + BaseDetailView, +): + """Export security compliance data for a single product.""" + + export_filename = "security_compliance" + export_fields = { + "vulnerability_id": "Vulnerability ID", + "aliases": "Aliases", + "summary": "Summary", + "risk_level": "Risk level", + "risk_score": "Risk score", + "exploitability": "Exploitability", + "weighted_severity": "Weighted severity", + "affected_package_count": "Affected packages", + "fixed_packages_count": "Fixed packages", + "resource_url": "Reference URL", + } + + def get(self, request, *args, **kwargs): + self.object = self.get_object() + return super().get(request, *args, **kwargs) + + def get_export_queryset(self): + product = self.object + vulnerabilities = product.get_vulnerability_qs(risk_threshold=None) + package_ids = product.productpackages.values_list("package_id", flat=True) + return vulnerabilities.annotate( + affected_package_count=Count( + "affected_packages", + filter=Q(affected_packages__in=package_ids), + distinct=True, + ), + ).order_by_risk() diff --git a/purldb/tests/test_purldb_toolkit.py b/purldb/tests/test_purldb_toolkit.py index 17ad45b8..324ef9ba 100644 --- a/purldb/tests/test_purldb_toolkit.py +++ b/purldb/tests/test_purldb_toolkit.py @@ -12,6 +12,7 @@ from dejacode_toolkit.purldb import PurlDB from dejacode_toolkit.purldb import pick_purldb_entry +from dejacode_toolkit.purldb import pick_source_package from dje.models import Dataspace from dje.tests import create_user @@ -75,3 +76,29 @@ def test_purldb_toolkit_pick_purldb_entry(self): self.assertEqual(entry2, pick_purldb_entry([entry1, entry2], purl=purl2)) self.assertIsNone(pick_purldb_entry([entry1, entry1], purl=purl1)) self.assertIsNone(pick_purldb_entry([entry1, entry2], purl=purl3)) + + def test_purldb_toolkit_pick_source_package(self): + self.assertIsNone(pick_source_package(None)) + self.assertIsNone(pick_source_package([])) + + entry_binary = { + "purl": "pkg:pypi/boto3@1.37.26?file_name=boto3-1.37.26-py3-none-any.whl", + "filename": "boto3-1.37.26-py3-none-any.whl", + "download_url": "https://files.pythonhosted.org/boto3-1.37.26-py3-none-any.whl", + "package_content": "binary", + } + entry_source = { + "purl": "pkg:pypi/boto3@1.37.26?file_name=boto3-1.37.26.tar.gz", + "filename": "boto3-1.37.26.tar.gz", + "download_url": "https://files.pythonhosted.org/boto3-1.37.26.tar.gz", + "package_content": "source_archive", + } + + self.assertEqual(entry_binary, pick_source_package([entry_binary])) + self.assertIsNone(pick_source_package([entry_binary, entry_binary])) + + self.assertEqual(entry_source, pick_source_package([entry_source])) + self.assertEqual(entry_source, pick_source_package([entry_source, entry_source])) + + self.assertEqual(entry_source, pick_source_package([entry_source, entry_binary])) + self.assertEqual(entry_source, pick_source_package([entry_binary, entry_source])) diff --git a/purldb/views.py b/purldb/views.py index 1c4233d8..401fb43b 100644 --- a/purldb/views.py +++ b/purldb/views.py @@ -15,8 +15,8 @@ from django.http import Http404 from django.template.defaultfilters import filesizeformat from django.urls import reverse -from django.utils.html import format_html from django.utils.html import format_html_join +from django.utils.html import mark_safe from django.utils.text import capfirst from django.utils.translation import gettext_lazy as _ from django.views.generic import DetailView @@ -94,7 +94,7 @@ def get_purldb_tab_fields(purldb_entry, dataspace): if field_name == "declared_license_expression": show_policy = dataspace.show_usage_policy_in_user_views licensing = get_dataspace_licensing(dataspace) - value = format_html(get_formatted_expression(licensing, value, show_policy)) + value = mark_safe(get_formatted_expression(licensing, value, show_policy)) elif field_name == "dependencies": value = json.dumps(value, indent=2) elif field_name == "size": @@ -138,7 +138,7 @@ def inject_license_expression_formatted(dataspace, object_list): expression = obj.get("declared_license_expression") if expression: formatted_expression = get_formatted_expression(licensing, expression, show_policy) - obj["license_expression_formatted"] = format_html(formatted_expression) + obj["license_expression_formatted"] = mark_safe(formatted_expression) return object_list diff --git a/pyproject.toml b/pyproject.toml index 966a8ea9..33b17b00 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,194 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "dejacode" +version = "5.7.1" +description = "Automate open source license compliance and ensure supply chain integrity" +readme = "README.rst" +requires-python = ">=3.14,<3.15" +license = "AGPL-3.0-only" +license-files = ["LICENSE", "NOTICE"] +authors = [ + { name = "nexB Inc.", email = "info@aboutcode.org" } +] +keywords = [ + "open source", "scan", "license", "package", "dependency", + "copyright", "filetype", "author", "extract", "licensing", + "scancode", "scanpipe", "docker", "rootfs", "vm", + "virtual machine", "pipeline", "code analysis", "container" +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Intended Audience :: Information Technology", + "Intended Audience :: Legal Industry", + "Programming Language :: Python", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.14", + "Topic :: Utilities" +] +dependencies = [ + # Base configuration tools + "setuptools==82.0.0", + "wheel==0.46.3", + "packaging==26.0", + "pip==26.0.1", + # Django + "Django==6.0.4", + "asgiref==3.11.1", + "typing_extensions==4.15.0", + "sqlparse==0.5.5", + # Django apps + "django-crispy-forms==2.6", + "crispy_bootstrap5==2026.3", + "django-grappelli==4.0.3", + "django-filter==25.2", + "django-registration==3.4", + "confusable_homoglyphs==3.3.1", + "django-guardian==3.3.0", + "django-environ==0.13.0", + "django-debug-toolbar==6.2.0", + # CAPTCHA + "altcha==1.0.0", + "django_altcha==0.10.0", + # REST API + "djangorestframework==3.16.1", + # API documentation + "drf-yasg==1.21.15", + "uritemplate==4.2.0", + "inflection==0.5.1", + "pytz==2025.2", + # Track failed login attempts + "django-axes==8.3.1", + # Multi-factor authentication + "django-otp==1.7.0", + "qrcode==8.2", + "pypng==0.20220715.0", + # Database + "psycopg==3.3.3", + # Cache + "redis==7.3.0", + # Antivirus + "clamd==1.0.2", + # Testing + "model_bakery==1.23.3", + # Task queue + "rq==2.7.0", + "croniter==6.2.2", + "django-rq==3.2.2", + "fakeredis==2.34.1", + # Libs + "certifi==2026.2.25", + "urllib3==2.6.3", + "python-dateutil==2.9.0.post0", + "python-mimeparse==2.0.0", + "PyJWT==2.12.1", + "natsort==8.4.0", + "six==1.17.0", + "requests==2.33.0", + "idna==3.11", + "charset-normalizer==3.4.4", + "PyYAML==6.0.3", + "cython==3.2.4", + "zipp==3.23.0", + "XlsxWriter==3.2.9", + # Markdown + "markdown==3.10.2", + "bleach==6.3.0", + "bleach_allowlist==1.0.3", + "webencodings==0.5.1", + # Authentication + "oauthlib==3.3.1", + "python3-openid==3.2.0", + "requests-oauthlib==2.0.0", + "defusedxml==0.7.1", + # LDAP Auth + "python_ldap==3.4.5", + "setuptools-scm==9.2.2", + "pyasn1==0.6.3", + "pyasn1-modules==0.4.2", + "django-auth-ldap==5.3.0", + # license expressions + "boolean.py==5.0", + "license-expression==30.4.4", + # Webhooks + "django-rest-hooks==1.6.1", + # django-notifications + "django_notifications_patched==2.0.0", + "jsonfield==3.2.0", + "swapper==1.4.0", + # AboutCode Toolkit + "aboutcode_toolkit==11.1.1", + "click==8.3.1", + "Jinja2==3.1.6", + "MarkupSafe==3.0.3", + "saneyaml==0.6.1", + "openpyxl==3.1.5", + "et-xmlfile==2.0.0", + # PackageURL + "packageurl-python==0.17.6", + # Gunicorn + "gunicorn==25.1.0", + # SPDX validation + "jsonschema==4.26.0", + "jsonschema-specifications==2025.9.1", + "referencing==0.37.0", + "rpds-py==0.30.0", + "attrs==25.4.0", + # CycloneDX + "cyclonedx-python-lib==11.6.0", + "sortedcontainers==2.4.0", + "py-serializable==2.1.0", + # Git + "GitPython==3.1.46", + "gitdb==4.0.12", + "smmap==5.0.2", + # CSAF + "pydantic==2.12.5", + "pydantic-core==2.41.5", + "typing-inspection==0.4.2", + "maturin==1.11.5", + "setuptools-rust==1.12.0", + "annotated-types==0.7.0", + "semantic-version==2.10.0", + # OpenVEX + "msgspec==0.20.0", + # OpenDocument Format + "odfdo==3.22.0", + "lxml==6.0.2", +] + +[project.optional-dependencies] +dev = [ + # Linter and Validation + "ruff==0.15.0", + # Parallel testing + "tblib==3.2.2" +] +docs = [ + "Sphinx", + "furo", + "doc8", +] + +[project.urls] +Homepage = "https://github.com/aboutcode-org/dejacode" +Documentation = "https://dejacode.readthedocs.io/" +Repository = "https://github.com/aboutcode-org/dejacode.git" +Issues = "https://github.com/aboutcode-org/dejacode/issues" +Changelog = "https://github.com/aboutcode-org/dejacode/blob/main/CHANGELOG.rst" + +[project.scripts] +dejacode = "dejacode:command_line" + +[tool.setuptools.packages.find] +where = ["."] + [tool.ruff] line-length = 100 +target-version = "py313" exclude = [ "migrations", "bin", @@ -11,6 +200,7 @@ exclude = [ "local", "var", "thirdparty", + "dejacode_toolkit/openvex/", ] [tool.ruff.lint] @@ -40,10 +230,11 @@ section-order = [ ] [tool.ruff.lint.mccabe] -max-complexity = 17 +max-complexity = 16 [tool.ruff.lint.per-file-ignores] # Do not run bandit on test files. "**/tests/*" = ["S"] "dejacode_toolkit/csaf/*" = ["D", "UP", "E501", "F401"] "dejacode_toolkit/spdx.py" = ["UP"] +"component_catalog/models.py" = ["C901"] diff --git a/reporting/admin.py b/reporting/admin.py index f1fe3564..fa4c97dd 100644 --- a/reporting/admin.py +++ b/reporting/admin.py @@ -13,6 +13,7 @@ from django.db.models import PositiveSmallIntegerField from django.forms import HiddenInput from django.utils.html import format_html +from django.utils.html import mark_safe from django.utils.http import urlencode from django.utils.translation import gettext_lazy as _ @@ -337,7 +338,7 @@ class ColumnTemplateAdmin(DataspacedAdmin): @admin.display(description=_("Assigned fields")) def get_field_names(self, instance): field_names = [assigned_field.field_name for assigned_field in instance.fields.all()] - return format_html("
        ".join(field_names)) + return mark_safe("
        ".join(field_names)) def get_queryset(self, request): qs = super().get_queryset(request) diff --git a/reporting/fields.py b/reporting/fields.py index d042e41b..fec089aa 100644 --- a/reporting/fields.py +++ b/reporting/fields.py @@ -12,7 +12,7 @@ from django.utils import timezone from django.utils.translation import gettext_lazy as _ -from dateutil import parser +import dateutil class BooleanSelect(Select): @@ -102,12 +102,8 @@ def __init__(self, attrs=None, choices=()): @staticmethod def _get_today(): - now = timezone.now() - # When time zone support is enabled, convert "now" to the user's time - # zone so Django's definition of "Today" matches what the user expects. - if timezone.is_aware(now): - now = timezone.localtime(now) - return now.replace(hour=0, minute=0, second=0, microsecond=0) + """Get today's date at midnight in the current timezone.""" + return timezone.localtime().replace(hour=0, minute=0, second=0, microsecond=0) def render(self, name, value, attrs=None, renderer=None): if not value: @@ -115,7 +111,10 @@ def render(self, name, value, attrs=None, renderer=None): value = "ERROR" try: - value_as_date = parser.parse(value) + value_as_date = dateutil.parser.parse(value) + # Make the parsed datetime timezone-aware to match "today" value + if timezone.is_naive(value_as_date): + value_as_date = timezone.make_aware(value_as_date) except Exception: value = "any_date" else: diff --git a/reporting/forms.py b/reporting/forms.py index 9d29a4f0..fa579cd8 100644 --- a/reporting/forms.py +++ b/reporting/forms.py @@ -219,7 +219,7 @@ def get_model_data_for_column_template(dataspace=None): "package_url", "short_package_url", "where_used", - "inferred_url", + "inferred_repo_url", "is_vulnerable", ], }, diff --git a/reporting/models.py b/reporting/models.py index f082656a..985629c7 100644 --- a/reporting/models.py +++ b/reporting/models.py @@ -26,7 +26,7 @@ from django.forms import fields_for_model from django.urls import NoReverseMatch from django.urls import reverse -from django.utils.html import format_html +from django.utils.html import mark_safe from django.utils.http import urlencode from django.utils.translation import gettext_lazy as _ @@ -37,6 +37,7 @@ from dje.models import is_secured from dje.models import secure_queryset_relational_fields from dje.utils import extract_name_version +from dje.utils import parse_date_aware from reporting.fields import DATE_FILTER_CHOICES from reporting.fields import BooleanSelect from reporting.fields import DateFieldFilterSelect @@ -358,6 +359,9 @@ def get_coerced_value(self, value): final_part = field_parts[-1] model_field_instance = model._meta.get_field(final_part) + if isinstance(model_field_instance, models.DateField): + value = parse_date_aware(value) + # For non-RelatedFields use the model field's form field to # coerce the value to a Python object if not isinstance(model_field_instance, RelatedField): @@ -368,10 +372,9 @@ def get_coerced_value(self, value): # is required to run the custom validators declared on the # Model field. model_field_instance.clean(value, model) - # We must check if ``fields_for_model()`` Return the field - # we are considering. For example ``AutoField`` returns - # None for its form field and thus will not be in the - # dictionary returned by ``fields_for_model()``. + # We must check if ``fields_for_model()`` Return the field we are considering. + # For example ``AutoField`` returns None for its form field and thus will not + # be in the dictionary returned by ``fields_for_model()``. form_field_instance = fields_for_model(model).get(final_part) if form_field_instance: widget_value = form_field_instance.widget.value_from_datadict( @@ -410,7 +413,7 @@ def get_q(self, runtime_value=None, user=None): if value == BooleanSelect.ALL_CHOICE_VALUE: return - # Hack to support special values for date filtering, see #9049 + # Hack to support special values for date filtering, such as "past_7_days" if value in [choice[0] for choice in DATE_FILTER_CHOICES]: value = DateFieldFilterSelect().value_from_datadict( data={"value": value}, files=None, name="value" @@ -547,7 +550,7 @@ def as_headers(self, include_view_link=False): """Return a list of field name, usable as Report results headers.""" headers = [field.display_name or field.field_name for field in self.fields.all()] if include_view_link: - headers.insert(0, format_html(" ")) + headers.insert(0, mark_safe(" ")) return headers @@ -764,7 +767,7 @@ def get_output(self, queryset=None, user=None, include_view_link=False, multi_as if queryset is None: queryset = self.query.get_qs(user=user) - icon = format_html('') + icon = mark_safe('') rows = [] for instance in queryset: diff --git a/reporting/templates/reporting/includes/report_list_table.html b/reporting/templates/reporting/includes/report_list_table.html index fe5d8bcb..47fba510 100644 --- a/reporting/templates/reporting/includes/report_list_table.html +++ b/reporting/templates/reporting/includes/report_list_table.html @@ -38,7 +38,7 @@
        - +
        diff --git a/reporting/templates/reporting/report_run.html b/reporting/templates/reporting/report_run.html index b852f533..a14fea63 100644 --- a/reporting/templates/reporting/report_run.html +++ b/reporting/templates/reporting/report_run.html @@ -15,7 +15,7 @@

        {{ object.name }} - + {% if user.is_staff and has_change_permission %} {% endif %} @@ -52,12 +52,13 @@

        diff --git a/reporting/tests/test_models.py b/reporting/tests/test_models.py index ba4a4d74..3adc1c8c 100644 --- a/reporting/tests/test_models.py +++ b/reporting/tests/test_models.py @@ -7,6 +7,7 @@ # import datetime +import zoneinfo from unittest.util import safe_repr from django.contrib.contenttypes.models import ContentType @@ -782,8 +783,27 @@ def test_get_coerced_value(self): lookup="exact", value="True", ) + self.assertEqual(True, f.get_coerced_value(f.value)) - expected = True + def test_get_coerced_value_date_field(self): + query = Query.objects.create( + dataspace=self.dataspace, + name="Date", + content_type=self.license_ct, + operator="and", + ) + f = Filter.objects.create( + dataspace=self.dataspace, + query=query, + field_name="last_modified_date", + lookup="gte", + value="2025-01-01", + ) + expected = datetime.datetime(2025, 1, 1, 0, 0, tzinfo=zoneinfo.ZoneInfo(key="UTC")) + self.assertEqual(expected, f.get_coerced_value(f.value)) + + f.update(value="2025-01-01 14:30:00") + expected = datetime.datetime(2025, 1, 1, 14, 30, tzinfo=zoneinfo.ZoneInfo(key="UTC")) self.assertEqual(expected, f.get_coerced_value(f.value)) def test_get_coerced_value_validation_from_model_validators(self): @@ -905,12 +925,9 @@ def test_get_q_for_date_field_filter(self): today = DateFieldFilterSelect._get_today() past_7_days = today - datetime.timedelta(days=7) - self.assertEqual([("last_modified_date__gte", str(past_7_days))], f.get_q().children) - - self.assertEqual([("last_modified_date__gte", str(today))], f.get_q("today").children) - - with self.assertRaises(ValidationError): - f.get_q("invalid").children + self.assertEqual([("last_modified_date__gte", past_7_days)], f.get_q().children) + self.assertEqual([("last_modified_date__gte", today)], f.get_q("today").children) + self.assertEqual([("last_modified_date__gte", None)], f.get_q("invalid").children) def test_get_q_for_boolean_select_all_choice_value(self): query = Query.objects.create( diff --git a/reporting/tests/test_views.py b/reporting/tests/test_views.py index 7949815e..ae34dcfa 100644 --- a/reporting/tests/test_views.py +++ b/reporting/tests/test_views.py @@ -370,6 +370,31 @@ def test_run_report_view_lookup_displayed_value(self): response = self.client.get(self.report.get_absolute_url()) self.assertContains(response, "Case-insensitive exact match.") + def test_run_report_view_date_field_filter_value(self): + self.client.login(username="test", password="t3st") + query = Query.objects.create( + dataspace=self.dataspace, + name="License activity", + content_type=ContentType.objects.get_for_model(License), + operator="or", + ) + Filter.objects.create( + dataspace=self.dataspace, + query=query, + field_name="last_modified_date", + lookup="gte", + value="2025-01-01", + runtime_parameter=True, + ) + report = Report.objects.create( + name="License activity", + query=query, + column_template=self.column_template, + ) + + response = self.client.get(report.get_absolute_url()) + self.assertEqual(200, response.status_code) + def test_run_report_view_results_count(self): self.client.login(username="test", password="t3st") response = self.client.get(self.report.get_absolute_url()) @@ -404,7 +429,7 @@ def test_report_view_get_json_response(self): [0, 1, 2], list(self.column_template.fields.all().values_list("seq", flat=True)) ) - # In case of a change: print repr(response.content) + # In case of a change: print(repr(response.content)) expected = ( "{\n " '"key": "license_126",\n ' @@ -423,10 +448,19 @@ def test_report_view_get_yaml_response(self): ) self.assertEqual(response["Content-Type"], "application/x-yaml") - # In case of a change: >>> print repr(response.content) + # In case of a change: >>> print(repr(response.content)) expected = "- key: license_138\n short_name: license_138\n name: license_138\n" self.assertContains(response, expected) + def test_report_view_get_ods_response(self): + self.client.login(username="test", password="t3st") + url = self.report.get_absolute_url() + "?format=ods" + response = self.client.get(url) + self.assertEqual( + response["Content-Disposition"], 'attachment; filename="license-list-for-analysis.ods"' + ) + self.assertEqual(response["Content-Type"], "application/vnd.oasis.opendocument.spreadsheet") + def test_run_report_view_runtime_parameters_value_fields(self): self.client.login(username="test", password="t3st") diff --git a/reporting/views.py b/reporting/views.py index afa00f2c..ac4c49d9 100644 --- a/reporting/views.py +++ b/reporting/views.py @@ -24,6 +24,7 @@ from django.views.generic.detail import SingleObjectMixin from django.views.generic.list import MultipleObjectMixin +import odfdo import saneyaml import xlsxwriter @@ -33,7 +34,7 @@ from dje.views import DataspacedFilterView from dje.views import DownloadableMixin from dje.views import HasPermissionMixin -from dje.views import PreviousNextPaginationMixin +from dje.views import PaginationMixin from reporting.filters import ReportFilterSet from reporting.forms import RuntimeFilterBaseFormSet from reporting.forms import RuntimeFilterForm @@ -80,7 +81,7 @@ def get(self, request, *args, **kwargs): class ReportDetailsView( LoginRequiredMixin, - PreviousNextPaginationMixin, + PaginationMixin, BootstrapCSSMixin, DownloadableMixin, HasPermissionMixin, @@ -232,6 +233,28 @@ def get_yaml_response(self, **response_kwargs): dump = self.get_dump(saneyaml.dump) return HttpResponse(dump, **response_kwargs) + def get_ods_response(self, **response_kwargs): + """Return the results as ods format.""" + context = self.get_context_data(**self.kwargs) + report_data = [context["headers"]] + context["output"] + + document = odfdo.Document("spreadsheet") + table = odfdo.Table("Report") + + for row_data in report_data: + row = odfdo.Row() + for cell_value in row_data: + row.append(odfdo.Cell(normalize_newlines(cell_value), cell_type="string")) + table.append(row) + + document.body.clear() + document.body.append(table) + + file_output = io.BytesIO() + document.save(file_output) + + return HttpResponse(file_output.getvalue(), **response_kwargs) + def get_xlsx_response(self, **response_kwargs): """Return the results as `xlsx` format.""" context = self.get_context_data(**self.kwargs) @@ -271,7 +294,6 @@ class ReportListView( filterset_class = ReportFilterSet template_name = "reporting/report_list.html" template_list_table = "reporting/includes/report_list_table.html" - paginate_by = 50 def get_queryset(self): qs = super().get_queryset() diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 63dd6665..00000000 --- a/setup.cfg +++ /dev/null @@ -1,201 +0,0 @@ -[metadata] -name = dejacode -version = 5.2.1 -license = AGPL-3.0-only -description = Automate open source license compliance and ensure supply chain integrity -long_description = file:README.rst -author = nexB Inc. -author_email = info@aboutcode.org -url = https://github.com/aboutcode-org/dejacode -classifiers = - Development Status :: 5 - Production/Stable - Intended Audience :: Developers - Intended Audience :: Information Technology - Intended Audience :: Legal Industry - Programming Language :: Python - Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.12 - Topic :: Utilities -keywords = - open source - scan - license - package - dependency - copyright - filetype - author - extract - licensing - scancode - scanpipe - docker - rootfs - vm - virtual machine - pipeline - code analysis - container -license_files = - LICENSE - NOTICE - -[options] -python_requires = >=3.12, <3.13 -packages=find: -include_package_data = true -zip_safe = false -install_requires = - # Base configuration tools - setuptools==75.8.0 - wheel==0.45.1 - pip==25.0.1 - # Django - Django==5.1.6 - asgiref==3.8.1 - typing_extensions==4.12.2 - sqlparse==0.5.3 - # Django apps - django-crispy-forms==2.3 - crispy_bootstrap5==2024.10 - django-grappelli==4.0.1 - django-filter==24.3 - django-registration==3.4 - confusable_homoglyphs==3.3.1 - django-hcaptcha-field==1.4.0 - django-guardian==2.4.0 - django-environ==0.12.0 - django-debug-toolbar==5.0.1 - # REST API - djangorestframework==3.15.2 - # API documentation, `coreapi` and its requirements: - coreapi==2.3.3 - MarkupSafe==3.0.2 - coreschema==0.0.4 - itypes==1.2.0 - Jinja2==3.1.5 - uritemplate==4.1.1 - # Access log - django-axes==5.35.0 - django-appconf==1.1.0 - django-ipware==7.0.1 - # Multi-factor authentication - django-otp==1.5.4 - qrcode==8.0 - pypng==0.20220715.0 - # Database - psycopg==3.2.4 - # Cache - redis==5.2.1 - # redis dependencies: - packaging==24.2 - pyparsing==3.2.1 - async-timeout==5.0.1 - Deprecated==1.2.18 - wrapt==1.17.2 - # Antivirus - clamd==1.0.2 - # Testing - model_bakery==1.10.1 - # Task queue - rq==2.1.0 - django-rq==3.0.0 - fakeredis==2.27.0 - # Scheduler - rq-scheduler==0.14.0 - crontab==1.0.1 - freezegun==1.5.1 - # Libs - certifi==2025.1.31 - urllib3==2.3.0 - python-dateutil==2.9.0.post0 - python-mimeparse==2.0.0 - PyJWT==2.10.1 - natsort==8.4.0 - six==1.17.0 - requests==2.32.3 - idna==3.10 - charset-normalizer==3.4.1 - PyYAML==6.0.2 - Cython==3.0.12 - importlib_metadata==8.6.1 - zipp==3.21.0 - XlsxWriter==3.2.2 - # Markdown - Markdown==3.7 - bleach==6.2.0 - bleach_allowlist==1.0.3 - webencodings==0.5.1 - # Authentication - oauthlib==3.2.2 - python3-openid==3.2.0 - requests-oauthlib==2.0.0 - defusedxml==0.7.1 - # LDAP Auth - python-ldap==3.4.4 - pyasn1==0.6.1 - pyasn1-modules==0.4.1 - django-auth-ldap==5.1.0 - # LDAP Testing - mockldap==0.3.0.post1 - funcparserlib==0.3.6 - # license expressions - boolean.py==4.0 - license-expression==30.4.1 - # Webhooks - django-rest-hooks==1.6.1 - # django-notifications - django_notifications_patched==2.0.0 - jsonfield==3.1.0 - swapper==1.4.0 - # AboutCode Toolkit - aboutcode_toolkit==11.0.0 - click==8.1.8 - saneyaml==0.6.1 - openpyxl==3.1.5 - et-xmlfile==2.0.0 - # PackageURL - packageurl-python==0.16.0 - # Gunicorn - gunicorn==23.0.0 - # SPDX validation - jsonschema==4.23.0 - jsonschema-specifications==2024.10.1 - referencing==0.36.2 - rpds-py==0.22.3 - attrs==25.1.0 - pyrsistent==0.20.0 - # CycloneDX - cyclonedx-python-lib==8.8.0 - sortedcontainers==2.4.0 - toml==0.10.2 - py-serializable==1.1.2 - # Git - GitPython==3.1.44 - gitdb==4.0.12 - smmap==5.0.2 - # CSAF - pydantic==2.10.6 - pydantic-core==2.27.2 - maturin==1.8.1 - setuptools-rust==1.10.2 - annotated-types==0.7.0 - semantic-version==2.10.0 - -[options.extras_require] -dev = - # Linter and Validation - ruff==0.9.6 - # Documentation - doc8==1.1.2 - stevedore==5.4.0 - Pygments==2.19.1 - docutils==0.21.2 - restructuredtext-lint==1.4.0 - pbr==6.1.1 - # Parallel testing - tblib==3.0.0 - -[options.entry_points] -console_scripts = - dejacode = dejacode:command_line diff --git a/thirdparty/dist/Cython-3.0.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl b/thirdparty/dist/Cython-3.0.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl deleted file mode 100644 index 67917dc7..00000000 Binary files a/thirdparty/dist/Cython-3.0.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl and /dev/null differ diff --git a/thirdparty/dist/Cython-3.0.12-py2.py3-none-any.whl b/thirdparty/dist/Cython-3.0.12-py2.py3-none-any.whl deleted file mode 100644 index 65226353..00000000 Binary files a/thirdparty/dist/Cython-3.0.12-py2.py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/Deprecated-1.2.18-py2.py3-none-any.whl b/thirdparty/dist/Deprecated-1.2.18-py2.py3-none-any.whl deleted file mode 100644 index 28a27e63..00000000 Binary files a/thirdparty/dist/Deprecated-1.2.18-py2.py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/Deprecated-1.2.18-py2.py3-none-any.whl.ABOUT b/thirdparty/dist/Deprecated-1.2.18-py2.py3-none-any.whl.ABOUT deleted file mode 100644 index 778c7012..00000000 --- a/thirdparty/dist/Deprecated-1.2.18-py2.py3-none-any.whl.ABOUT +++ /dev/null @@ -1,14 +0,0 @@ -about_resource: Deprecated-1.2.18-py2.py3-none-any.whl -name: deprecated -version: 1.2.18 -download_url: https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl -package_url: pkg:pypi/deprecated@1.2.18 -license_expression: mit -copyright: Copyright deprecated project contributors -attribute: yes -checksum_md5: aef0a32d82c9ba85b949ea93d27a43cb -checksum_sha1: ab7d0a40ba273683cc3ba3cd4d2f987fc097def1 -licenses: - - key: mit - name: MIT License - file: mit.LICENSE diff --git a/thirdparty/dist/Django-5.1.6-py3-none-any.whl.ABOUT b/thirdparty/dist/Django-5.1.6-py3-none-any.whl.ABOUT deleted file mode 100644 index 77e6f2bb..00000000 --- a/thirdparty/dist/Django-5.1.6-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,14 +0,0 @@ -about_resource: Django-5.1.6-py3-none-any.whl -name: django -version: 5.1.6 -download_url: https://files.pythonhosted.org/packages/75/6f/d2c216d00975e2604b10940937b0ba6b2c2d9b3cc0cc633e414ae3f14b2e/Django-5.1.6-py3-none-any.whl -package_url: pkg:pypi/django@5.1.6 -license_expression: bsd-new -copyright: Copyright django project contributors -attribute: yes -checksum_md5: 27ad2093eaef2c1aeb76cb8025cbbbf5 -checksum_sha1: 70a0ffed34bb16e4373f95e91018bde4d0ef1b2f -licenses: - - key: bsd-new - name: BSD-3-Clause - file: bsd-new.LICENSE diff --git a/thirdparty/dist/GitPython-3.1.44-py3-none-any.whl b/thirdparty/dist/GitPython-3.1.44-py3-none-any.whl deleted file mode 100644 index 42a5ffc8..00000000 Binary files a/thirdparty/dist/GitPython-3.1.44-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/GitPython-3.1.44-py3-none-any.whl.ABOUT b/thirdparty/dist/GitPython-3.1.44-py3-none-any.whl.ABOUT deleted file mode 100644 index 38be17b1..00000000 --- a/thirdparty/dist/GitPython-3.1.44-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,14 +0,0 @@ -about_resource: GitPython-3.1.44-py3-none-any.whl -name: gitpython -version: 3.1.44 -download_url: https://files.pythonhosted.org/packages/1d/9a/4114a9057db2f1462d5c8f8390ab7383925fe1ac012eaa42402ad65c2963/GitPython-3.1.44-py3-none-any.whl -package_url: pkg:pypi/gitpython@3.1.44 -license_expression: bsd-new -copyright: Copyright gitpython project contributors -attribute: yes -checksum_md5: 01f7b1c5f5e54ead375eadd25b72ad74 -checksum_sha1: 6ed3638b5a7223401743c653e5ef64634c242dc6 -licenses: - - key: bsd-new - name: BSD-3-Clause - file: bsd-new.LICENSE diff --git a/thirdparty/dist/Markdown-3.7-py3-none-any.whl b/thirdparty/dist/Markdown-3.7-py3-none-any.whl deleted file mode 100644 index 50320296..00000000 Binary files a/thirdparty/dist/Markdown-3.7-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/Markdown-3.7-py3-none-any.whl.ABOUT b/thirdparty/dist/Markdown-3.7-py3-none-any.whl.ABOUT deleted file mode 100644 index d52dc701..00000000 --- a/thirdparty/dist/Markdown-3.7-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,14 +0,0 @@ -about_resource: Markdown-3.7-py3-none-any.whl -name: markdown -version: '3.7' -download_url: https://files.pythonhosted.org/packages/3f/08/83871f3c50fc983b88547c196d11cf8c3340e37c32d2e9d6152abe2c61f7/Markdown-3.7-py3-none-any.whl -package_url: pkg:pypi/markdown@3.7 -license_expression: bsd-new -copyright: Copyright The Python Markdown Project, Yuri Takhteyev, Manfred Stienstra -attribute: yes -checksum_md5: b8116c94af09aa5b83cef992fa2328b7 -checksum_sha1: d1612629884c2f4483fdecf39107399b70220fbd -licenses: - - key: bsd-new - name: BSD-3-Clause - file: bsd-new.LICENSE diff --git a/thirdparty/dist/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl b/thirdparty/dist/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl deleted file mode 100644 index 0c29579b..00000000 Binary files a/thirdparty/dist/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl and /dev/null differ diff --git a/thirdparty/dist/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl b/thirdparty/dist/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl deleted file mode 100644 index 12902271..00000000 Binary files a/thirdparty/dist/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl and /dev/null differ diff --git a/thirdparty/dist/PyJWT-2.10.1-py3-none-any.whl b/thirdparty/dist/PyJWT-2.10.1-py3-none-any.whl deleted file mode 100644 index 6195c576..00000000 Binary files a/thirdparty/dist/PyJWT-2.10.1-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/PyJWT-2.10.1-py3-none-any.whl.ABOUT b/thirdparty/dist/PyJWT-2.10.1-py3-none-any.whl.ABOUT deleted file mode 100644 index 6eac7a69..00000000 --- a/thirdparty/dist/PyJWT-2.10.1-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,14 +0,0 @@ -about_resource: PyJWT-2.10.1-py3-none-any.whl -name: pyjwt -version: 2.10.1 -download_url: https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl -package_url: pkg:pypi/pyjwt@2.10.1 -license_expression: mit -copyright: Copyright pyjwt project contributors -attribute: yes -checksum_md5: dcd961185ca574bdf86b92026d714cb6 -checksum_sha1: b10d7244e16cc654d5c91ba9cf82174658afb93c -licenses: - - key: mit - name: MIT License - file: mit.LICENSE diff --git a/thirdparty/dist/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl b/thirdparty/dist/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl deleted file mode 100644 index 3de6b6b9..00000000 Binary files a/thirdparty/dist/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl and /dev/null differ diff --git a/thirdparty/dist/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl b/thirdparty/dist/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl deleted file mode 100644 index 901226c9..00000000 Binary files a/thirdparty/dist/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl and /dev/null differ diff --git a/thirdparty/dist/PyYAML-6.0.tar.gz b/thirdparty/dist/PyYAML-6.0.tar.gz deleted file mode 100644 index 82259d5b..00000000 Binary files a/thirdparty/dist/PyYAML-6.0.tar.gz and /dev/null differ diff --git a/thirdparty/dist/PyYAML-6.0.tar.gz.ABOUT b/thirdparty/dist/PyYAML-6.0.tar.gz.ABOUT deleted file mode 100644 index 907d3261..00000000 --- a/thirdparty/dist/PyYAML-6.0.tar.gz.ABOUT +++ /dev/null @@ -1,31 +0,0 @@ -about_resource: PyYAML-6.0.tar.gz -name: pyyaml -version: '6.0' -download_url: https://files.pythonhosted.org/packages/36/2b/61d51a2c4f25ef062ae3f74576b01638bebad5e045f747ff12643df63844/PyYAML-6.0.tar.gz -description: | - YAML parser and emitter for Python - YAML is a data serialization format designed for human readability - and interaction with scripting languages. PyYAML is a YAML parser - and emitter for Python. - - PyYAML features a complete YAML 1.1 parser, Unicode support, pickle - support, capable extension API, and sensible error messages. PyYAML - supports standard YAML tags and provides Python-specific tags that - allow to represent an arbitrary Python object. - - PyYAML is applicable for a broad range of tasks from complex - configuration files to object serialization and persistence. -homepage_url: https://pyyaml.org/ -package_url: pkg:pypi/pyyaml@6.0 -license_expression: mit -copyright: Copyright (c) Ingy döt Net, Kirill Simonov -notice_file: PyYAML-6.0.tar.gz.NOTICE -attribute: yes -owner: Kirill Simonov -owner_url: http://www.yaml.org/ -checksum_md5: 1d19c798f25e58e3e582f0f8c977dbb8 -checksum_sha1: a8c40a3ae9d4c159382a58db3153d83e5521c51e -licenses: - - key: mit - name: MIT License - file: mit.LICENSE diff --git a/thirdparty/dist/PyYAML-6.0.tar.gz.NOTICE b/thirdparty/dist/PyYAML-6.0.tar.gz.NOTICE deleted file mode 100644 index a3fdf73d..00000000 --- a/thirdparty/dist/PyYAML-6.0.tar.gz.NOTICE +++ /dev/null @@ -1,17 +0,0 @@ -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. \ No newline at end of file diff --git a/thirdparty/dist/XlsxWriter-3.2.2-py3-none-any.whl b/thirdparty/dist/XlsxWriter-3.2.2-py3-none-any.whl deleted file mode 100644 index 04969fab..00000000 Binary files a/thirdparty/dist/XlsxWriter-3.2.2-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/XlsxWriter-3.2.2-py3-none-any.whl.ABOUT b/thirdparty/dist/XlsxWriter-3.2.2-py3-none-any.whl.ABOUT deleted file mode 100644 index 52398ebb..00000000 --- a/thirdparty/dist/XlsxWriter-3.2.2-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,17 +0,0 @@ -about_resource: XlsxWriter-3.2.2-py3-none-any.whl -name: xlsxwriter -version: 3.2.2 -download_url: https://files.pythonhosted.org/packages/9b/07/df054f7413bdfff5e98f75056e4ed0977d0c8716424011fac2587864d1d3/XlsxWriter-3.2.2-py3-none-any.whl -package_url: pkg:pypi/xlsxwriter@3.2.2 -license_expression: bsd-new AND bsd-simplified -copyright: Copyright xlsxwriter project contributors -attribute: yes -checksum_md5: 508abf735526e464ab5e27217ac67fc8 -checksum_sha1: e1adf91f979a74da3860024b4f7824ebbf740210 -licenses: - - key: bsd-simplified - name: BSD-2-Clause - file: bsd-simplified.LICENSE - - key: bsd-new - name: BSD-3-Clause - file: bsd-new.LICENSE diff --git a/thirdparty/dist/aboutcode_toolkit-11.0.0-py3-none-any.whl b/thirdparty/dist/aboutcode_toolkit-11.0.0-py3-none-any.whl deleted file mode 100644 index 29a3e71f..00000000 Binary files a/thirdparty/dist/aboutcode_toolkit-11.0.0-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/aboutcode_toolkit-11.0.0-py3-none-any.whl.ABOUT b/thirdparty/dist/aboutcode_toolkit-11.0.0-py3-none-any.whl.ABOUT deleted file mode 100644 index b9946c42..00000000 --- a/thirdparty/dist/aboutcode_toolkit-11.0.0-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,15 +0,0 @@ -about_resource: aboutcode_toolkit-11.0.0-py3-none-any.whl -name: aboutcode-toolkit -version: 11.0.0 -download_url: https://files.pythonhosted.org/packages/2a/00/c5603b3d776b5614b422d762de76b20b678a18c179a63668f56b8b924ce3/aboutcode_toolkit-11.0.0-py3-none-any.whl -package_url: pkg:pypi/aboutcode-toolkit@11.0.0 -license_expression: apache-2.0 -copyright: Copyright aboutcode-toolkit project contributors -attribute: yes -track_changes: yes -checksum_md5: 6109bd4bba3847f37ad4d4b082f0ea85 -checksum_sha1: 448bc6a426fff397e4fb745a72c4d3e60f0c5634 -licenses: - - key: apache-2.0 - name: Apache License 2.0 - file: apache-2.0.LICENSE diff --git a/thirdparty/dist/aboutcode_toolkit-11.1.1-py3-none-any.whl b/thirdparty/dist/aboutcode_toolkit-11.1.1-py3-none-any.whl new file mode 100644 index 00000000..bb0c59d6 Binary files /dev/null and b/thirdparty/dist/aboutcode_toolkit-11.1.1-py3-none-any.whl differ diff --git a/thirdparty/dist/aboutcode_toolkit-11.1.1-py3-none-any.whl.ABOUT b/thirdparty/dist/aboutcode_toolkit-11.1.1-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..76db1ab4 --- /dev/null +++ b/thirdparty/dist/aboutcode_toolkit-11.1.1-py3-none-any.whl.ABOUT @@ -0,0 +1,15 @@ +about_resource: aboutcode_toolkit-11.1.1-py3-none-any.whl +name: aboutcode-toolkit +version: 11.1.1 +download_url: https://files.pythonhosted.org/packages/14/ee/ba139231e3de1287189c2f7940e7e0f8a135421050ff1b4f0145813ae8b9/aboutcode_toolkit-11.1.1-py3-none-any.whl +package_url: pkg:pypi/aboutcode-toolkit@11.1.1 +license_expression: apache-2.0 +copyright: Copyright aboutcode-toolkit project contributors +attribute: yes +track_changes: yes +checksum_md5: 8b274d62b7d390eb2ae080d90035d3be +checksum_sha1: a72560aac87a281507122ad580590a265b0bd17b +licenses: + - key: apache-2.0 + name: Apache License 2.0 + file: apache-2.0.LICENSE diff --git a/thirdparty/dist/altcha-1.0.0-py3-none-any.whl b/thirdparty/dist/altcha-1.0.0-py3-none-any.whl new file mode 100644 index 00000000..b0640718 Binary files /dev/null and b/thirdparty/dist/altcha-1.0.0-py3-none-any.whl differ diff --git a/thirdparty/dist/altcha-1.0.0-py3-none-any.whl.ABOUT b/thirdparty/dist/altcha-1.0.0-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..8f942f0c --- /dev/null +++ b/thirdparty/dist/altcha-1.0.0-py3-none-any.whl.ABOUT @@ -0,0 +1,14 @@ +about_resource: altcha-1.0.0-py3-none-any.whl +name: altcha +version: 1.0.0 +download_url: https://files.pythonhosted.org/packages/88/19/978dac535829b3e9175427d4d91a34b0ec6e120375a38fabd9840acf32ca/altcha-1.0.0-py3-none-any.whl +package_url: pkg:pypi/altcha@1.0.0 +license_expression: mit +copyright: Copyright altcha project contributors +attribute: yes +checksum_md5: 5c401435f14e149797f6b77343138ca4 +checksum_sha1: 402795bf7e9eaa34159fb974a1d61fb0616a54fd +licenses: + - key: mit + name: MIT License + file: mit.LICENSE diff --git a/thirdparty/dist/annotated_types-0.7.0-py3-none-any.whl.ABOUT b/thirdparty/dist/annotated_types-0.7.0-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..72f5a255 --- /dev/null +++ b/thirdparty/dist/annotated_types-0.7.0-py3-none-any.whl.ABOUT @@ -0,0 +1,14 @@ +about_resource: annotated_types-0.7.0-py3-none-any.whl +name: annotated-types +version: 0.7.0 +download_url: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl +package_url: pkg:pypi/annotated-types@0.7.0 +license_expression: mit +copyright: Copyright annotated-types project contributors +attribute: yes +checksum_md5: b132aea373e91e5a46e3b5425c9970e2 +checksum_sha1: ea2dd11b884eb6bc96d35b81d764010aa19eb952 +licenses: + - key: mit + name: MIT License + file: mit.LICENSE diff --git a/thirdparty/dist/asgiref-3.11.1-py3-none-any.whl b/thirdparty/dist/asgiref-3.11.1-py3-none-any.whl new file mode 100644 index 00000000..ac7db18e Binary files /dev/null and b/thirdparty/dist/asgiref-3.11.1-py3-none-any.whl differ diff --git a/thirdparty/dist/asgiref-3.11.1-py3-none-any.whl.ABOUT b/thirdparty/dist/asgiref-3.11.1-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..2e911efc --- /dev/null +++ b/thirdparty/dist/asgiref-3.11.1-py3-none-any.whl.ABOUT @@ -0,0 +1,14 @@ +about_resource: asgiref-3.11.1-py3-none-any.whl +name: asgiref +version: 3.11.1 +download_url: https://files.pythonhosted.org/packages/5c/0a/a72d10ed65068e115044937873362e6e32fab1b7dce0046aeb224682c989/asgiref-3.11.1-py3-none-any.whl +package_url: pkg:pypi/asgiref@3.11.1 +license_expression: bsd-new +copyright: Copyright asgiref project contributors +attribute: yes +checksum_md5: 45c208eee6ba3a4af25735a12bbeb363 +checksum_sha1: 9c6062b3b9eb39e14fafe0c07f329b6e4bf9dbee +licenses: + - key: bsd-new + name: BSD-3-Clause + file: bsd-new.LICENSE diff --git a/thirdparty/dist/asgiref-3.8.1-py3-none-any.whl b/thirdparty/dist/asgiref-3.8.1-py3-none-any.whl deleted file mode 100644 index 244dc23b..00000000 Binary files a/thirdparty/dist/asgiref-3.8.1-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/asgiref-3.8.1-py3-none-any.whl.ABOUT b/thirdparty/dist/asgiref-3.8.1-py3-none-any.whl.ABOUT deleted file mode 100644 index d30187d1..00000000 --- a/thirdparty/dist/asgiref-3.8.1-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,14 +0,0 @@ -about_resource: asgiref-3.8.1-py3-none-any.whl -name: asgiref -version: 3.8.1 -download_url: https://files.pythonhosted.org/packages/39/e3/893e8757be2612e6c266d9bb58ad2e3651524b5b40cf56761e985a28b13e/asgiref-3.8.1-py3-none-any.whl -package_url: pkg:pypi/asgiref@3.8.1 -license_expression: bsd-new -copyright: Copyright asgiref project contributors -attribute: yes -checksum_md5: 87ef2494bf2bb208cd3652e577c74bc9 -checksum_sha1: b66c85ef24257bdf0f415febcbffcf5f09dc7d83 -licenses: - - key: bsd-new - name: BSD-3-Clause - file: bsd-new.LICENSE diff --git a/thirdparty/dist/async_timeout-5.0.1-py3-none-any.whl b/thirdparty/dist/async_timeout-5.0.1-py3-none-any.whl deleted file mode 100644 index 585d57c6..00000000 Binary files a/thirdparty/dist/async_timeout-5.0.1-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/async_timeout-5.0.1-py3-none-any.whl.ABOUT b/thirdparty/dist/async_timeout-5.0.1-py3-none-any.whl.ABOUT deleted file mode 100644 index 20e9d1fb..00000000 --- a/thirdparty/dist/async_timeout-5.0.1-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,15 +0,0 @@ -about_resource: async_timeout-5.0.1-py3-none-any.whl -name: async-timeout -version: 5.0.1 -download_url: https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl -package_url: pkg:pypi/async-timeout@5.0.1 -license_expression: apache-2.0 -copyright: Copyright async-timeout project contributors -attribute: yes -track_changes: yes -checksum_md5: efe854d55a96998187975472b17f7774 -checksum_sha1: a78078860feb93fe8e02602e3dc3580f6b7cf3f3 -licenses: - - key: apache-2.0 - name: Apache License 2.0 - file: apache-2.0.LICENSE diff --git a/thirdparty/dist/attrs-25.1.0-py3-none-any.whl b/thirdparty/dist/attrs-25.1.0-py3-none-any.whl deleted file mode 100644 index e9c60f1e..00000000 Binary files a/thirdparty/dist/attrs-25.1.0-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/attrs-25.1.0-py3-none-any.whl.ABOUT b/thirdparty/dist/attrs-25.1.0-py3-none-any.whl.ABOUT deleted file mode 100644 index 09771866..00000000 --- a/thirdparty/dist/attrs-25.1.0-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,17 +0,0 @@ -about_resource: attrs-25.1.0-py3-none-any.whl -name: attrs -version: 25.1.0 -download_url: https://files.pythonhosted.org/packages/fc/30/d4986a882011f9df997a55e6becd864812ccfcd821d64aac8570ee39f719/attrs-25.1.0-py3-none-any.whl -package_url: pkg:pypi/attrs@25.1.0 -license_expression: mit AND unknown-license-reference -copyright: Copyright attrs project contributors -attribute: yes -checksum_md5: e6b450578edf429c8622cfe481b0879b -checksum_sha1: f65c5b0524ef7a6d099e3bada05881fb695f9209 -licenses: - - key: mit - name: MIT License - file: mit.LICENSE - - key: unknown-license-reference - name: Unknown License file reference - file: unknown-license-reference.LICENSE diff --git a/thirdparty/dist/attrs-25.4.0-py3-none-any.whl b/thirdparty/dist/attrs-25.4.0-py3-none-any.whl new file mode 100644 index 00000000..55be7ece Binary files /dev/null and b/thirdparty/dist/attrs-25.4.0-py3-none-any.whl differ diff --git a/thirdparty/dist/attrs-25.4.0-py3-none-any.whl.ABOUT b/thirdparty/dist/attrs-25.4.0-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..aa47bdf0 --- /dev/null +++ b/thirdparty/dist/attrs-25.4.0-py3-none-any.whl.ABOUT @@ -0,0 +1,17 @@ +about_resource: attrs-25.4.0-py3-none-any.whl +name: attrs +version: 25.4.0 +download_url: https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl +package_url: pkg:pypi/attrs@25.4.0 +license_expression: mit AND unknown-license-reference +copyright: Copyright attrs project contributors +attribute: yes +checksum_md5: 7b7fab960686d9e318a640d5ddce32f7 +checksum_sha1: 0f44b024e556094358b37aa227f07cdd70baffa9 +licenses: + - key: mit + name: MIT License + file: mit.LICENSE + - key: unknown-license-reference + name: Unknown License file reference + file: unknown-license-reference.LICENSE diff --git a/thirdparty/dist/bleach-6.2.0-py3-none-any.whl.ABOUT b/thirdparty/dist/bleach-6.2.0-py3-none-any.whl.ABOUT deleted file mode 100644 index bf3c6b62..00000000 --- a/thirdparty/dist/bleach-6.2.0-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,18 +0,0 @@ -about_resource: bleach-6.2.0-py3-none-any.whl -name: bleach -version: 6.2.0 -download_url: https://files.pythonhosted.org/packages/fc/55/96142937f66150805c25c4d0f31ee4132fd33497753400734f9dfdcbdc66/bleach-6.2.0-py3-none-any.whl -package_url: pkg:pypi/bleach@6.2.0 -license_expression: apache-2.0 AND mit -copyright: Copyright bleach project contributors -attribute: yes -track_changes: yes -checksum_md5: 660e2e515dcab76aaba61c1ef9dac291 -checksum_sha1: 413fc1f05fd745e698b8b93068d3ba3760574e19 -licenses: - - key: apache-2.0 - name: Apache License 2.0 - file: apache-2.0.LICENSE - - key: mit - name: MIT License - file: mit.LICENSE diff --git a/thirdparty/dist/bleach-6.2.0-py3-none-any.whl b/thirdparty/dist/bleach-6.3.0-py3-none-any.whl similarity index 70% rename from thirdparty/dist/bleach-6.2.0-py3-none-any.whl rename to thirdparty/dist/bleach-6.3.0-py3-none-any.whl index bd65dc29..6dd8ebad 100644 Binary files a/thirdparty/dist/bleach-6.2.0-py3-none-any.whl and b/thirdparty/dist/bleach-6.3.0-py3-none-any.whl differ diff --git a/thirdparty/dist/bleach-6.3.0-py3-none-any.whl.ABOUT b/thirdparty/dist/bleach-6.3.0-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..7814d78e --- /dev/null +++ b/thirdparty/dist/bleach-6.3.0-py3-none-any.whl.ABOUT @@ -0,0 +1,18 @@ +about_resource: bleach-6.3.0-py3-none-any.whl +name: bleach +version: 6.3.0 +download_url: https://files.pythonhosted.org/packages/cd/3a/577b549de0cc09d95f11087ee63c739bba856cd3952697eec4c4bb91350a/bleach-6.3.0-py3-none-any.whl +package_url: pkg:pypi/bleach@6.3.0 +license_expression: apache-2.0 AND mit +copyright: Copyright bleach project contributors +attribute: yes +track_changes: yes +checksum_md5: 582f05dac01de36bf93c8e05b9cba11b +checksum_sha1: 74792becf1c32fb1edd3e04594acaee490969170 +licenses: + - key: apache-2.0 + name: Apache License 2.0 + file: apache-2.0.LICENSE + - key: mit + name: MIT License + file: mit.LICENSE diff --git a/thirdparty/dist/boolean.py-4.0-py3-none-any.whl b/thirdparty/dist/boolean.py-4.0-py3-none-any.whl deleted file mode 100644 index e5d93a37..00000000 Binary files a/thirdparty/dist/boolean.py-4.0-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/boolean.py-4.0-py3-none-any.whl.ABOUT b/thirdparty/dist/boolean.py-4.0-py3-none-any.whl.ABOUT deleted file mode 100644 index 79de370e..00000000 --- a/thirdparty/dist/boolean.py-4.0-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,16 +0,0 @@ -about_resource: boolean.py-4.0-py3-none-any.whl -name: boolean.py -version: '4.0' -download_url: https://files.pythonhosted.org/packages/3f/02/6389ef0529af6da0b913374dedb9bbde8eabfe45767ceec38cc37801b0bd/boolean.py-4.0-py3-none-any.whl -homepage_url: https://pypi.org/project/boolean.py/4.0/ -package_url: pkg:pypi/boolean.py@4.0 -license_expression: bsd-simplified -copyright: Copyright Sebastian Kraemer, and others -attribute: yes -owner: Sebastian Kraemer -checksum_md5: 73208a6fc38d6904a1d7e793e8da1292 -checksum_sha1: f0d0381ba22c35ca8727289172010c186fc83b95 -licenses: - - key: bsd-simplified - name: BSD-2-Clause - file: bsd-simplified.LICENSE diff --git a/thirdparty/dist/boolean_py-5.0-py3-none-any.whl b/thirdparty/dist/boolean_py-5.0-py3-none-any.whl new file mode 100644 index 00000000..d8efeec2 Binary files /dev/null and b/thirdparty/dist/boolean_py-5.0-py3-none-any.whl differ diff --git a/thirdparty/dist/boolean_py-5.0-py3-none-any.whl.ABOUT b/thirdparty/dist/boolean_py-5.0-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..46ead243 --- /dev/null +++ b/thirdparty/dist/boolean_py-5.0-py3-none-any.whl.ABOUT @@ -0,0 +1,14 @@ +about_resource: boolean_py-5.0-py3-none-any.whl +name: boolean-py +version: '5.0' +download_url: https://files.pythonhosted.org/packages/e5/ca/78d423b324b8d77900030fa59c4aa9054261ef0925631cd2501dd015b7b7/boolean_py-5.0-py3-none-any.whl +package_url: pkg:pypi/boolean-py@5.0 +license_expression: bsd-simplified +copyright: Copyright Sebastian Kraemer, and others +attribute: yes +checksum_md5: df9060a88bfb6ba3b9314783b16a0faa +checksum_sha1: 626d017b8ba4acb25c2231a4a428bf364e582b2a +licenses: + - key: bsd-simplified + name: BSD-2-Clause + file: bsd-simplified.LICENSE diff --git a/thirdparty/dist/certifi-2025.1.31-py3-none-any.whl b/thirdparty/dist/certifi-2025.1.31-py3-none-any.whl deleted file mode 100644 index d2b3bb74..00000000 Binary files a/thirdparty/dist/certifi-2025.1.31-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/certifi-2025.1.31-py3-none-any.whl.ABOUT b/thirdparty/dist/certifi-2025.1.31-py3-none-any.whl.ABOUT deleted file mode 100644 index c9490a60..00000000 --- a/thirdparty/dist/certifi-2025.1.31-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,16 +0,0 @@ -about_resource: certifi-2025.1.31-py3-none-any.whl -name: certifi -version: 2025.1.31 -download_url: https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl -package_url: pkg:pypi/certifi@2025.1.31 -license_expression: mpl-2.0 -copyright: Copyright certifi project contributors -redistribute: yes -attribute: yes -track_changes: yes -checksum_md5: c205c7808745af1661fb0fba4df49238 -checksum_sha1: 6c1dbe778d4c8fa2e7324a34bd4e1705b530e7d8 -licenses: - - key: mpl-2.0 - name: Mozilla Public License 2.0 - file: mpl-2.0.LICENSE diff --git a/thirdparty/dist/certifi-2026.2.25-py3-none-any.whl b/thirdparty/dist/certifi-2026.2.25-py3-none-any.whl new file mode 100644 index 00000000..9d7fbf09 Binary files /dev/null and b/thirdparty/dist/certifi-2026.2.25-py3-none-any.whl differ diff --git a/thirdparty/dist/certifi-2026.2.25-py3-none-any.whl.ABOUT b/thirdparty/dist/certifi-2026.2.25-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..9d149150 --- /dev/null +++ b/thirdparty/dist/certifi-2026.2.25-py3-none-any.whl.ABOUT @@ -0,0 +1,16 @@ +about_resource: certifi-2026.2.25-py3-none-any.whl +name: certifi +version: 2026.2.25 +download_url: https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl +package_url: pkg:pypi/certifi@2026.2.25 +license_expression: mpl-2.0 +copyright: Copyright certifi project contributors +redistribute: yes +attribute: yes +track_changes: yes +checksum_md5: 1719fecdcfb531a622c0ee93e6bf4ba1 +checksum_sha1: a932814817c6bc9b17b21bda8e1fac40dce626b2 +licenses: + - key: mpl-2.0 + name: Mozilla Public License 2.0 + file: mpl-2.0.LICENSE diff --git a/thirdparty/dist/cffi-1.15.0.tar.gz b/thirdparty/dist/cffi-1.15.0.tar.gz deleted file mode 100644 index 9bc39f87..00000000 Binary files a/thirdparty/dist/cffi-1.15.0.tar.gz and /dev/null differ diff --git a/thirdparty/dist/cffi-1.15.0.tar.gz.ABOUT b/thirdparty/dist/cffi-1.15.0.tar.gz.ABOUT deleted file mode 100644 index 5f9b6657..00000000 --- a/thirdparty/dist/cffi-1.15.0.tar.gz.ABOUT +++ /dev/null @@ -1,30 +0,0 @@ -about_resource: cffi-1.15.0.tar.gz -name: cffi -version: 1.15.0 -download_url: https://files.pythonhosted.org/packages/00/9e/92de7e1217ccc3d5f352ba21e52398372525765b2e0c4530e6eb2ba9282a/cffi-1.15.0.tar.gz -description: | - CFFI - ==== - - Foreign Function Interface for Python calling C code. - Please see the `Documentation `_. - - Contact - ------- - - `Mailing list `_ -homepage_url: http://cffi.readthedocs.org -package_url: pkg:pypi/cffi@1.15.0 -license_expression: mit -copyright: Copyright (c) cffi project conributors -notice_file: cffi-1.15.0.tar.gz.NOTICE -attribute: yes -owner: Python CFFI -owner_url: https://github.com/python-cffi -contact: https://cffi.readthedocs.io/en/latest/ -checksum_md5: f3a3f26cd3335fc597479c9475da0a0b -checksum_sha1: 9c51c29e35510adf7f94542e1f8e05611930b07b -licenses: - - key: mit - name: MIT License - file: mit.LICENSE diff --git a/thirdparty/dist/cffi-1.15.0.tar.gz.NOTICE b/thirdparty/dist/cffi-1.15.0.tar.gz.NOTICE deleted file mode 100644 index 433e4f62..00000000 --- a/thirdparty/dist/cffi-1.15.0.tar.gz.NOTICE +++ /dev/null @@ -1,24 +0,0 @@ -Except when otherwise stated (look for LICENSE files in directories or -information at the beginning of each file) all software and -documentation is licensed as follows: - - The MIT License - - 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. diff --git a/thirdparty/dist/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl b/thirdparty/dist/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl deleted file mode 100644 index 561a2153..00000000 Binary files a/thirdparty/dist/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl and /dev/null differ diff --git a/thirdparty/dist/charset_normalizer-3.4.1-py3-none-any.whl b/thirdparty/dist/charset_normalizer-3.4.1-py3-none-any.whl deleted file mode 100644 index 54bf48e7..00000000 Binary files a/thirdparty/dist/charset_normalizer-3.4.1-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/charset_normalizer-3.4.1-py3-none-any.whl.ABOUT b/thirdparty/dist/charset_normalizer-3.4.1-py3-none-any.whl.ABOUT deleted file mode 100644 index f00d297f..00000000 --- a/thirdparty/dist/charset_normalizer-3.4.1-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,14 +0,0 @@ -about_resource: charset_normalizer-3.4.1-py3-none-any.whl -name: charset-normalizer -version: 3.4.1 -download_url: https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl -package_url: pkg:pypi/charset-normalizer@3.4.1 -license_expression: mit -copyright: Copyright Ahmed TAHRI @Ousret, Denny Vrandecic, TAHRI Ahmed R. -attribute: yes -checksum_md5: 4c824e22b968c4efaa2a436464e438a5 -checksum_sha1: 480257e54141d777c7d593535c0de10b70b08216 -licenses: - - key: mit - name: MIT License - file: mit.LICENSE diff --git a/thirdparty/dist/charset_normalizer-3.4.4-py3-none-any.whl b/thirdparty/dist/charset_normalizer-3.4.4-py3-none-any.whl new file mode 100644 index 00000000..13507564 Binary files /dev/null and b/thirdparty/dist/charset_normalizer-3.4.4-py3-none-any.whl differ diff --git a/thirdparty/dist/charset_normalizer-3.4.4-py3-none-any.whl.ABOUT b/thirdparty/dist/charset_normalizer-3.4.4-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..f0c4892d --- /dev/null +++ b/thirdparty/dist/charset_normalizer-3.4.4-py3-none-any.whl.ABOUT @@ -0,0 +1,14 @@ +about_resource: charset_normalizer-3.4.4-py3-none-any.whl +name: charset-normalizer +version: 3.4.4 +download_url: https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl +package_url: pkg:pypi/charset-normalizer@3.4.4 +license_expression: mit +copyright: Copyright Ahmed TAHRI @Ousret, Denny Vrandecic, TAHRI Ahmed R. +attribute: yes +checksum_md5: a425e0aabe24dde0df1bc1eaca464c5b +checksum_sha1: f8e205046cfeb85815f79705565c73be32e5c193 +licenses: + - key: mit + name: MIT License + file: mit.LICENSE diff --git a/thirdparty/dist/click-8.1.8-py3-none-any.whl b/thirdparty/dist/click-8.1.8-py3-none-any.whl deleted file mode 100644 index db2c6b31..00000000 Binary files a/thirdparty/dist/click-8.1.8-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/click-8.1.8-py3-none-any.whl.ABOUT b/thirdparty/dist/click-8.1.8-py3-none-any.whl.ABOUT deleted file mode 100644 index 6d9f2450..00000000 --- a/thirdparty/dist/click-8.1.8-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,14 +0,0 @@ -about_resource: click-8.1.8-py3-none-any.whl -name: click -version: 8.1.8 -download_url: https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl -package_url: pkg:pypi/click@8.1.8 -license_expression: bsd-new -copyright: Copyright click project contributors -attribute: yes -checksum_md5: 7dc0eee374f3bb75bcce4c9dd4222f5f -checksum_sha1: 80a7480700f1aff8340ddbc68a3dba98471889d3 -licenses: - - key: bsd-new - name: BSD-3-Clause - file: bsd-new.LICENSE diff --git a/thirdparty/dist/click-8.3.1-py3-none-any.whl b/thirdparty/dist/click-8.3.1-py3-none-any.whl new file mode 100644 index 00000000..f2513adc Binary files /dev/null and b/thirdparty/dist/click-8.3.1-py3-none-any.whl differ diff --git a/thirdparty/dist/click-8.3.1-py3-none-any.whl.ABOUT b/thirdparty/dist/click-8.3.1-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..879c24bf --- /dev/null +++ b/thirdparty/dist/click-8.3.1-py3-none-any.whl.ABOUT @@ -0,0 +1,17 @@ +about_resource: click-8.3.1-py3-none-any.whl +name: click +version: 8.3.1 +download_url: https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl +package_url: pkg:pypi/click@8.3.1 +license_expression: bsd-new AND unknown-license-reference +copyright: Copyright click project contributors +attribute: yes +checksum_md5: f032502934a5979330da77e3f09d889c +checksum_sha1: 4645f956db291ad4bdb38a84f1e27ea87ca19a37 +licenses: + - key: bsd-new + name: BSD-3-Clause + file: bsd-new.LICENSE + - key: unknown-license-reference + name: Unknown License file reference + file: unknown-license-reference.LICENSE diff --git a/thirdparty/dist/coreapi-2.3.3-py2.py3-none-any.whl b/thirdparty/dist/coreapi-2.3.3-py2.py3-none-any.whl deleted file mode 100644 index 452ef750..00000000 Binary files a/thirdparty/dist/coreapi-2.3.3-py2.py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/coreapi-2.3.3-py2.py3-none-any.whl.ABOUT b/thirdparty/dist/coreapi-2.3.3-py2.py3-none-any.whl.ABOUT deleted file mode 100644 index 95bd02ee..00000000 --- a/thirdparty/dist/coreapi-2.3.3-py2.py3-none-any.whl.ABOUT +++ /dev/null @@ -1,20 +0,0 @@ -about_resource: coreapi-2.3.3-py2.py3-none-any.whl -name: coreapi -version: 2.3.3 -download_url: https://pypi.python.org/packages/fc/3a/9dedaad22962770edd334222f2b3c3e7ad5e1c8cab1d6a7992c30329e2e5/coreapi-2.3.3-py2.py3-none-any.whl#md5=05f392bc31296d90377f1a2f1cd1e271 -description: Python client library for Core API. -homepage_url: https://github.com/core-api/python-client -package_url: pkg:pypi/coreapi@2.3.3 -license_expression: bsd-simplified -copyright: Copyright 2015-2016, Tom Christie -notice_file: coreapi-2.3.3-py2.py3-none-any.whl.NOTICE -attribute: yes -owner: Tom Christie -owner_url: https://github.com/tomchristie -vcs_repository: https://github.com/core-api/python-client.git -checksum_md5: 05f392bc31296d90377f1a2f1cd1e271 -checksum_sha1: 0ae2fd413f8966c63d188b1e43c70acf73779c9b -licenses: - - key: bsd-simplified - name: BSD-2-Clause - file: bsd-simplified.LICENSE diff --git a/thirdparty/dist/coreapi-2.3.3-py2.py3-none-any.whl.NOTICE b/thirdparty/dist/coreapi-2.3.3-py2.py3-none-any.whl.NOTICE deleted file mode 100644 index 9e947d42..00000000 --- a/thirdparty/dist/coreapi-2.3.3-py2.py3-none-any.whl.NOTICE +++ /dev/null @@ -1,8 +0,0 @@ -Metadata-Version: 2.0 -Name: coreapi -Version: 2.3.3 -Summary: Python client library for Core API. -Home-page: https://github.com/core-api/python-client -Author: Tom Christie -Author-email: tom@tomchristie.com -License: BSD \ No newline at end of file diff --git a/thirdparty/dist/coreschema-0.0.4.tar.gz b/thirdparty/dist/coreschema-0.0.4.tar.gz deleted file mode 100644 index b10b1cb7..00000000 Binary files a/thirdparty/dist/coreschema-0.0.4.tar.gz and /dev/null differ diff --git a/thirdparty/dist/coreschema-0.0.4.tar.gz.ABOUT b/thirdparty/dist/coreschema-0.0.4.tar.gz.ABOUT deleted file mode 100644 index 542d35d3..00000000 --- a/thirdparty/dist/coreschema-0.0.4.tar.gz.ABOUT +++ /dev/null @@ -1,22 +0,0 @@ -about_resource: coreschema-0.0.4.tar.gz -name: coreschema -version: 0.0.4 -download_url: https://pypi.python.org/packages/93/08/1d105a70104e078718421e6c555b8b293259e7fc92f7e9a04869947f198f/coreschema-0.0.4.tar.gz#md5=144cb7262fc9e3024b20fa606343fd32 -description: | - Core API is a format-independent Document Object Model for representing Web APIs. It can be used to represent either Schema or Hypermedia responses, and allows you to interact with an API at the layer of an application interface, rather than a network interface. Core API can be used to interact with any API that exposes a supported Schema or Hypermedia format. - - See http://www.coreapi.org/ for more information. -homepage_url: https://github.com/core-api/python-coreschema -package_url: pkg:pypi/coreschema@0.0.4 -license_expression: bsd-simplified -copyright: Copyright (c) Tom Christie -notice_file: coreschema-0.0.4.tar.gz.NOTICE -attribute: yes -owner: Tom Christie -owner_url: https://github.com/tomchristie -checksum_md5: 144cb7262fc9e3024b20fa606343fd32 -checksum_sha1: 36d2d193b28cb6e22debd7b10665a2a2eb97288b -licenses: - - key: bsd-simplified - name: BSD-2-Clause - file: bsd-simplified.LICENSE diff --git a/thirdparty/dist/coreschema-0.0.4.tar.gz.NOTICE b/thirdparty/dist/coreschema-0.0.4.tar.gz.NOTICE deleted file mode 100644 index fe785655..00000000 --- a/thirdparty/dist/coreschema-0.0.4.tar.gz.NOTICE +++ /dev/null @@ -1,8 +0,0 @@ -Metadata-Version: 1.1 -Name: coreschema -Version: 0.0.4 -Summary: Core Schema. -Home-page: https://github.com/core-api/python-coreschema -Author: Tom Christie -Author-email: tom@tomchristie.com -License: BSD \ No newline at end of file diff --git a/thirdparty/dist/crispy_bootstrap5-2024.10-py3-none-any.whl b/thirdparty/dist/crispy_bootstrap5-2024.10-py3-none-any.whl deleted file mode 100644 index 604f2651..00000000 Binary files a/thirdparty/dist/crispy_bootstrap5-2024.10-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/crispy_bootstrap5-2024.2-py3-none-any.whl b/thirdparty/dist/crispy_bootstrap5-2024.2-py3-none-any.whl deleted file mode 100644 index 716381af..00000000 Binary files a/thirdparty/dist/crispy_bootstrap5-2024.2-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/crispy_bootstrap5-2024.2-py3-none-any.whl.ABOUT b/thirdparty/dist/crispy_bootstrap5-2024.2-py3-none-any.whl.ABOUT deleted file mode 100644 index 925b63cf..00000000 --- a/thirdparty/dist/crispy_bootstrap5-2024.2-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,14 +0,0 @@ -about_resource: crispy_bootstrap5-2024.2-py3-none-any.whl -name: crispy-bootstrap5 -version: '2024.2' -download_url: https://files.pythonhosted.org/packages/13/11/a1c22b4c803125901af7d2fa683f4c6eb14d511644da5fc0b45a0cb44cae/crispy_bootstrap5-2024.2-py3-none-any.whl -package_url: pkg:pypi/crispy-bootstrap5@2024.2 -license_expression: mit -copyright: Copyright crispy-bootstrap5 project contributors -attribute: yes -checksum_md5: 92fc180cf23546d83bcf5c62e9167c63 -checksum_sha1: fa73bb9ef292376ed0de04d043b8f70a85c3bdb6 -licenses: - - key: mit - name: MIT License - file: mit.LICENSE diff --git a/thirdparty/dist/crispy_bootstrap5-2026.3-py3-none-any.whl b/thirdparty/dist/crispy_bootstrap5-2026.3-py3-none-any.whl new file mode 100644 index 00000000..7f271b15 Binary files /dev/null and b/thirdparty/dist/crispy_bootstrap5-2026.3-py3-none-any.whl differ diff --git a/thirdparty/dist/crispy_bootstrap5-2026.3-py3-none-any.whl.ABOUT b/thirdparty/dist/crispy_bootstrap5-2026.3-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..821b6635 --- /dev/null +++ b/thirdparty/dist/crispy_bootstrap5-2026.3-py3-none-any.whl.ABOUT @@ -0,0 +1,17 @@ +about_resource: crispy_bootstrap5-2026.3-py3-none-any.whl +name: crispy-bootstrap5 +version: '2026.3' +download_url: https://files.pythonhosted.org/packages/9d/2d/93f78072f203aa28d961add6a130b929b47f7aa3fef6905898b4bb9a637d/crispy_bootstrap5-2026.3-py3-none-any.whl +package_url: pkg:pypi/crispy-bootstrap5@2026.3 +license_expression: mit AND unknown-license-reference +copyright: Copyright crispy-bootstrap5 project contributors +attribute: yes +checksum_md5: cfb86388b260ba60735c2d156ee26dcc +checksum_sha1: 065b9b385fdfad76a4c499771affa9328ff14370 +licenses: + - key: mit + name: MIT License + file: mit.LICENSE + - key: unknown-license-reference + name: Unknown License file reference + file: unknown-license-reference.LICENSE diff --git a/thirdparty/dist/croniter-6.2.2-py3-none-any.whl b/thirdparty/dist/croniter-6.2.2-py3-none-any.whl new file mode 100644 index 00000000..22e19587 Binary files /dev/null and b/thirdparty/dist/croniter-6.2.2-py3-none-any.whl differ diff --git a/thirdparty/dist/croniter-6.2.2-py3-none-any.whl.ABOUT b/thirdparty/dist/croniter-6.2.2-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..abdb4e37 --- /dev/null +++ b/thirdparty/dist/croniter-6.2.2-py3-none-any.whl.ABOUT @@ -0,0 +1,14 @@ +about_resource: croniter-6.2.2-py3-none-any.whl +name: croniter +version: 6.2.2 +download_url: https://files.pythonhosted.org/packages/d0/39/783980e78cb92c2d7bdb1fc7dbc86e94ccc6d58224d76a7f1f51b6c51e30/croniter-6.2.2-py3-none-any.whl +package_url: pkg:pypi/croniter@6.2.2 +license_expression: mit +copyright: Copyright croniter project contributors +attribute: yes +checksum_md5: f082e4f95beab0874ed9691ed41d6e54 +checksum_sha1: 688b5a8465f65bec362e67b576c8dbbc899931fa +licenses: + - key: mit + name: MIT License + file: mit.LICENSE diff --git a/thirdparty/dist/crontab-1.0.1.tar.gz b/thirdparty/dist/crontab-1.0.1.tar.gz deleted file mode 100644 index 097e390a..00000000 Binary files a/thirdparty/dist/crontab-1.0.1.tar.gz and /dev/null differ diff --git a/thirdparty/dist/cyclonedx_python_lib-11.6.0-py3-none-any.whl b/thirdparty/dist/cyclonedx_python_lib-11.6.0-py3-none-any.whl new file mode 100644 index 00000000..49ec9b27 Binary files /dev/null and b/thirdparty/dist/cyclonedx_python_lib-11.6.0-py3-none-any.whl differ diff --git a/thirdparty/dist/cyclonedx_python_lib-11.6.0-py3-none-any.whl.ABOUT b/thirdparty/dist/cyclonedx_python_lib-11.6.0-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..8c4fc73f --- /dev/null +++ b/thirdparty/dist/cyclonedx_python_lib-11.6.0-py3-none-any.whl.ABOUT @@ -0,0 +1,15 @@ +about_resource: cyclonedx_python_lib-11.6.0-py3-none-any.whl +name: cyclonedx-python-lib +version: 11.6.0 +download_url: https://files.pythonhosted.org/packages/c7/1b/534ad8a5e0f9470522811a8e5a9bc5d328fb7738ba29faf357467a4ef6d0/cyclonedx_python_lib-11.6.0-py3-none-any.whl +package_url: pkg:pypi/cyclonedx-python-lib@11.6.0 +license_expression: apache-2.0 +copyright: Copyright OWASP Foundation +attribute: yes +track_changes: yes +checksum_md5: 1f8a970a5307c851ff4ab8f2836db736 +checksum_sha1: 2272f348d83800d8450edbc7ce52974386e7f229 +licenses: + - key: apache-2.0 + name: Apache License 2.0 + file: apache-2.0.LICENSE diff --git a/thirdparty/dist/cyclonedx_python_lib-8.8.0-py3-none-any.whl b/thirdparty/dist/cyclonedx_python_lib-8.8.0-py3-none-any.whl deleted file mode 100644 index 9ca42da2..00000000 Binary files a/thirdparty/dist/cyclonedx_python_lib-8.8.0-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/cyclonedx_python_lib-8.8.0-py3-none-any.whl.ABOUT b/thirdparty/dist/cyclonedx_python_lib-8.8.0-py3-none-any.whl.ABOUT deleted file mode 100644 index 4af1b3aa..00000000 --- a/thirdparty/dist/cyclonedx_python_lib-8.8.0-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,15 +0,0 @@ -about_resource: cyclonedx_python_lib-8.8.0-py3-none-any.whl -name: cyclonedx-python-lib -version: 8.8.0 -download_url: https://files.pythonhosted.org/packages/12/11/8a75e0995a7c9c885eadc65806149dbdd206cf67ab3a8a8d7169e97cf69a/cyclonedx_python_lib-8.8.0-py3-none-any.whl -package_url: pkg:pypi/cyclonedx-python-lib@8.8.0 -license_expression: apache-2.0 -copyright: Copyright OWASP Foundation -attribute: yes -track_changes: yes -checksum_md5: db7e22a9e0a5717d0a233a1c227943fb -checksum_sha1: 754735804a0d2249fdf0ea14613fe5e67b733bcd -licenses: - - key: apache-2.0 - name: Apache License 2.0 - file: apache-2.0.LICENSE diff --git a/thirdparty/dist/cython-3.2.4-cp314-cp314-macosx_11_0_arm64.whl b/thirdparty/dist/cython-3.2.4-cp314-cp314-macosx_11_0_arm64.whl new file mode 100644 index 00000000..cf9951ae Binary files /dev/null and b/thirdparty/dist/cython-3.2.4-cp314-cp314-macosx_11_0_arm64.whl differ diff --git a/thirdparty/dist/cython-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl b/thirdparty/dist/cython-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl new file mode 100644 index 00000000..81cdf083 Binary files /dev/null and b/thirdparty/dist/cython-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl differ diff --git a/thirdparty/dist/cython-3.2.4-py3-none-any.whl b/thirdparty/dist/cython-3.2.4-py3-none-any.whl new file mode 100644 index 00000000..c0c0c8df Binary files /dev/null and b/thirdparty/dist/cython-3.2.4-py3-none-any.whl differ diff --git a/thirdparty/dist/cython-3.2.4-py3-none-any.whl.ABOUT b/thirdparty/dist/cython-3.2.4-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..a1ae1346 --- /dev/null +++ b/thirdparty/dist/cython-3.2.4-py3-none-any.whl.ABOUT @@ -0,0 +1,15 @@ +about_resource: cython-3.2.4-py3-none-any.whl +name: cython +version: 3.2.4 +download_url: https://files.pythonhosted.org/packages/ff/fa/d3c15189f7c52aaefbaea76fb012119b04b9013f4bf446cb4eb4c26c4e6b/cython-3.2.4-py3-none-any.whl +package_url: pkg:pypi/cython@3.2.4 +license_expression: apache-2.0 +copyright: Copyright cython project contributors +attribute: yes +track_changes: yes +checksum_md5: cb1da7746fd0fc2077f78f5ae89a4c3c +checksum_sha1: bbbedf37da4b846172f234097b1191e2d260e171 +licenses: + - key: apache-2.0 + name: Apache License 2.0 + file: apache-2.0.LICENSE diff --git a/thirdparty/dist/Django-5.1.6-py3-none-any.whl b/thirdparty/dist/django-6.0.4-py3-none-any.whl similarity index 68% rename from thirdparty/dist/Django-5.1.6-py3-none-any.whl rename to thirdparty/dist/django-6.0.4-py3-none-any.whl index 7785d9b2..772704d5 100644 Binary files a/thirdparty/dist/Django-5.1.6-py3-none-any.whl and b/thirdparty/dist/django-6.0.4-py3-none-any.whl differ diff --git a/thirdparty/dist/django-6.0.4-py3-none-any.whl.ABOUT b/thirdparty/dist/django-6.0.4-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..9f56a17d --- /dev/null +++ b/thirdparty/dist/django-6.0.4-py3-none-any.whl.ABOUT @@ -0,0 +1,21 @@ +about_resource: django-6.0.4-py3-none-any.whl +name: django +version: 6.0.4 +download_url: https://files.pythonhosted.org/packages/e9/47/3d61d611609764aa71a37f7037b870e7bfb22937366974c4fd46cada7bab/django-6.0.4-py3-none-any.whl +package_url: pkg:pypi/django@6.0.4 +license_expression: bsd-new AND python AND unknown-license-reference +copyright: Copyright django project contributors +attribute: yes +track_changes: yes +checksum_md5: 48574fa2e00fde976bd35d62f336bcd7 +checksum_sha1: b1e01ebdd99e6d06de34a8e92e62da256eaf5e8e +licenses: + - key: bsd-new + name: BSD-3-Clause + file: bsd-new.LICENSE + - key: python + name: Python Software Foundation License v2 + file: python.LICENSE + - key: unknown-license-reference + name: Unknown License file reference + file: unknown-license-reference.LICENSE diff --git a/thirdparty/dist/django_altcha-0.10.0-py3-none-any.whl b/thirdparty/dist/django_altcha-0.10.0-py3-none-any.whl new file mode 100644 index 00000000..20a4852e Binary files /dev/null and b/thirdparty/dist/django_altcha-0.10.0-py3-none-any.whl differ diff --git a/thirdparty/dist/django_altcha-0.10.0-py3-none-any.whl.ABOUT b/thirdparty/dist/django_altcha-0.10.0-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..9c5b3b38 --- /dev/null +++ b/thirdparty/dist/django_altcha-0.10.0-py3-none-any.whl.ABOUT @@ -0,0 +1,17 @@ +about_resource: django_altcha-0.10.0-py3-none-any.whl +name: django-altcha +version: 0.10.0 +download_url: https://files.pythonhosted.org/packages/cd/0c/d1872b7464e0f7c76eb3b39d6785d9725133c2e7169a0b8ba696fb6d64ae/django_altcha-0.10.0-py3-none-any.whl +package_url: pkg:pypi/django-altcha@0.10.0 +license_expression: mit AND unknown-license-reference +copyright: Copyright Daniel Regeci +attribute: yes +checksum_md5: b97bf310858aab066e3e5258f42ab18b +checksum_sha1: 48c0ab74e1baee2a6f10d7fbe10a38ff968063ae +licenses: + - key: mit + name: MIT License + file: mit.LICENSE + - key: unknown-license-reference + name: Unknown License file reference + file: unknown-license-reference.LICENSE diff --git a/thirdparty/dist/django_appconf-1.1.0-py3-none-any.whl b/thirdparty/dist/django_appconf-1.1.0-py3-none-any.whl deleted file mode 100644 index 39a5e508..00000000 Binary files a/thirdparty/dist/django_appconf-1.1.0-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/django_appconf-1.1.0-py3-none-any.whl.ABOUT b/thirdparty/dist/django_appconf-1.1.0-py3-none-any.whl.ABOUT deleted file mode 100644 index 8d5694a6..00000000 --- a/thirdparty/dist/django_appconf-1.1.0-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,14 +0,0 @@ -about_resource: django_appconf-1.1.0-py3-none-any.whl -name: django-appconf -version: 1.1.0 -download_url: https://files.pythonhosted.org/packages/62/9e/f3a899991e4aaae4b69c1aa187ba4a32e34742475c91eb13010ee7fbe9db/django_appconf-1.1.0-py3-none-any.whl -package_url: pkg:pypi/django-appconf@1.1.0 -license_expression: bsd-new -copyright: Copyright django-appconf project contributors -attribute: yes -checksum_md5: e47a5eafb8fc17609c7bff5701176421 -checksum_sha1: b48f3a059915364284cf9d27200bd00a8f11592f -licenses: - - key: bsd-new - name: BSD-3-Clause - file: bsd-new.LICENSE diff --git a/thirdparty/dist/django_auth_ldap-5.1.0-py3-none-any.whl b/thirdparty/dist/django_auth_ldap-5.1.0-py3-none-any.whl deleted file mode 100644 index 61b4870d..00000000 Binary files a/thirdparty/dist/django_auth_ldap-5.1.0-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/django_auth_ldap-5.1.0-py3-none-any.whl.ABOUT b/thirdparty/dist/django_auth_ldap-5.1.0-py3-none-any.whl.ABOUT deleted file mode 100644 index e0f1e16a..00000000 --- a/thirdparty/dist/django_auth_ldap-5.1.0-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,14 +0,0 @@ -about_resource: django_auth_ldap-5.1.0-py3-none-any.whl -name: django-auth-ldap -version: 5.1.0 -download_url: https://files.pythonhosted.org/packages/11/47/f3492884addbb17672cc9a6053381162010d6e40ccd8440dedf22f72bc7f/django_auth_ldap-5.1.0-py3-none-any.whl -package_url: pkg:pypi/django-auth-ldap@5.1.0 -license_expression: bsd-new -copyright: Copyright django-auth-ldap project contributors -attribute: yes -checksum_md5: 8437fc5a4b6d597d718df49cf3f2ca20 -checksum_sha1: a289f44511dfceff1704188e7493cbfd5ea3320a -licenses: - - key: bsd-new - name: BSD-3-Clause - file: bsd-new.LICENSE diff --git a/thirdparty/dist/django_auth_ldap-5.3.0-py3-none-any.whl b/thirdparty/dist/django_auth_ldap-5.3.0-py3-none-any.whl new file mode 100644 index 00000000..5d07f5dd Binary files /dev/null and b/thirdparty/dist/django_auth_ldap-5.3.0-py3-none-any.whl differ diff --git a/thirdparty/dist/django_auth_ldap-5.3.0-py3-none-any.whl.ABOUT b/thirdparty/dist/django_auth_ldap-5.3.0-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..5b460d5f --- /dev/null +++ b/thirdparty/dist/django_auth_ldap-5.3.0-py3-none-any.whl.ABOUT @@ -0,0 +1,14 @@ +about_resource: django_auth_ldap-5.3.0-py3-none-any.whl +name: django-auth-ldap +version: 5.3.0 +download_url: https://files.pythonhosted.org/packages/a9/91/38ba24b9d76925ce166b2eebe1b4ea460063b8ba8cf91d39d97ee3bad517/django_auth_ldap-5.3.0-py3-none-any.whl +package_url: pkg:pypi/django-auth-ldap@5.3.0 +license_expression: bsd-new +copyright: Copyright django-auth-ldap project contributors +attribute: yes +checksum_md5: 1cdf18b1be5d570d14813f1c41b90308 +checksum_sha1: b3cf94cf797280d009666f3f1fc7a668388331ea +licenses: + - key: bsd-new + name: BSD-3-Clause + file: bsd-new.LICENSE diff --git a/thirdparty/dist/django_axes-5.35.0-py3-none-any.whl b/thirdparty/dist/django_axes-5.35.0-py3-none-any.whl deleted file mode 100644 index 96d97182..00000000 Binary files a/thirdparty/dist/django_axes-5.35.0-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/django_axes-5.35.0-py3-none-any.whl.ABOUT b/thirdparty/dist/django_axes-5.35.0-py3-none-any.whl.ABOUT deleted file mode 100644 index 8b6f3fb0..00000000 --- a/thirdparty/dist/django_axes-5.35.0-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,17 +0,0 @@ -about_resource: django_axes-5.35.0-py3-none-any.whl -name: django-axes -version: 5.35.0 -download_url: https://files.pythonhosted.org/packages/43/79/6c0bf6e79509f9eafa065dc67d45f7677a5ede84a66d3234ec68febd12da/django_axes-5.35.0-py3-none-any.whl -homepage_url: https://pypi.org/project/django-axes/5.35.0/ -package_url: pkg:pypi/django-axes@5.35.0 -license_expression: mit -copyright: Copyright (c) Josh VanderLinden and project contributors -notice_file: django_axes-5.35.0-py3-none-any.whl.NOTICE -attribute: yes -owner: Jazzband -checksum_md5: 9363ef6d095c3043c5ad8bc3bd95d159 -checksum_sha1: 8fc4797a178eee7251de7dc3bc59e2160396f9cf -licenses: - - key: mit - name: MIT License - file: mit.LICENSE diff --git a/thirdparty/dist/django_axes-5.35.0-py3-none-any.whl.NOTICE b/thirdparty/dist/django_axes-5.35.0-py3-none-any.whl.NOTICE deleted file mode 100644 index 98a91365..00000000 --- a/thirdparty/dist/django_axes-5.35.0-py3-none-any.whl.NOTICE +++ /dev/null @@ -1 +0,0 @@ -License :: OSI Approved :: MIT License \ No newline at end of file diff --git a/thirdparty/dist/django_axes-8.3.1-py3-none-any.whl b/thirdparty/dist/django_axes-8.3.1-py3-none-any.whl new file mode 100644 index 00000000..036877fe Binary files /dev/null and b/thirdparty/dist/django_axes-8.3.1-py3-none-any.whl differ diff --git a/thirdparty/dist/django_axes-8.3.1-py3-none-any.whl.ABOUT b/thirdparty/dist/django_axes-8.3.1-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..00a1fad5 --- /dev/null +++ b/thirdparty/dist/django_axes-8.3.1-py3-none-any.whl.ABOUT @@ -0,0 +1,14 @@ +about_resource: django_axes-8.3.1-py3-none-any.whl +name: django-axes +version: 8.3.1 +download_url: https://files.pythonhosted.org/packages/46/f6/777a5df7d5698eeb23c7c496cc268c0181f0489dbc5b5f2b235b30e5d8dd/django_axes-8.3.1-py3-none-any.whl +package_url: pkg:pypi/django-axes@8.3.1 +license_expression: mit +copyright: Copyright django-axes project contributors +attribute: yes +checksum_md5: 36393eb8d62a7caf22185a31414a752a +checksum_sha1: fafe2e62da5f8181d80760656ca4948d16cf8524 +licenses: + - key: mit + name: MIT License + file: mit.LICENSE diff --git a/thirdparty/dist/django_crispy_forms-2.1-py3-none-any.whl b/thirdparty/dist/django_crispy_forms-2.1-py3-none-any.whl deleted file mode 100644 index 28477013..00000000 Binary files a/thirdparty/dist/django_crispy_forms-2.1-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/django_crispy_forms-2.1-py3-none-any.whl.ABOUT b/thirdparty/dist/django_crispy_forms-2.1-py3-none-any.whl.ABOUT deleted file mode 100644 index 558380ff..00000000 --- a/thirdparty/dist/django_crispy_forms-2.1-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,14 +0,0 @@ -about_resource: django_crispy_forms-2.1-py3-none-any.whl -name: django-crispy-forms -version: '2.1' -download_url: https://files.pythonhosted.org/packages/58/b4/0fc9eed10828d538b0706e64ec3b8c32e42539ba9c6245d17ad4a3873163/django_crispy_forms-2.1-py3-none-any.whl -package_url: pkg:pypi/django-crispy-forms@2.1 -license_expression: mit -copyright: Copyright django-crispy-forms project contributors -attribute: yes -checksum_md5: 42afc54ee22275b489a5b9e962022186 -checksum_sha1: a723f5fdab15363f05b5b57f62b9d6a88a7913f1 -licenses: - - key: mit - name: MIT License - file: mit.LICENSE diff --git a/thirdparty/dist/django_crispy_forms-2.3-py3-none-any.whl b/thirdparty/dist/django_crispy_forms-2.3-py3-none-any.whl deleted file mode 100644 index 2680dfa3..00000000 Binary files a/thirdparty/dist/django_crispy_forms-2.3-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/django_crispy_forms-2.6-py3-none-any.whl b/thirdparty/dist/django_crispy_forms-2.6-py3-none-any.whl new file mode 100644 index 00000000..582dfd8b Binary files /dev/null and b/thirdparty/dist/django_crispy_forms-2.6-py3-none-any.whl differ diff --git a/thirdparty/dist/django_crispy_forms-2.6-py3-none-any.whl.ABOUT b/thirdparty/dist/django_crispy_forms-2.6-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..a9997c72 --- /dev/null +++ b/thirdparty/dist/django_crispy_forms-2.6-py3-none-any.whl.ABOUT @@ -0,0 +1,13 @@ +about_resource: django_crispy_forms-2.6-py3-none-any.whl +name: django-crispy-forms +version: '2.6' +download_url: https://files.pythonhosted.org/packages/96/e3/4c5915a732d6ab54da8871400852b67529518eedfb6b78ecf10bbccfcabb/django_crispy_forms-2.6-py3-none-any.whl +package_url: pkg:pypi/django-crispy-forms@2.6 +license_expression: unknown-license-reference +copyright: Copyright django-crispy-forms project contributors +checksum_md5: 6c45e06bdef246a29905cf96d1300036 +checksum_sha1: 2d5827887f31a44c0309278ca5fa2e7821063801 +licenses: + - key: unknown-license-reference + name: Unknown License file reference + file: unknown-license-reference.LICENSE diff --git a/thirdparty/dist/django_debug_toolbar-5.0.1-py3-none-any.whl.ABOUT b/thirdparty/dist/django_debug_toolbar-5.0.1-py3-none-any.whl.ABOUT deleted file mode 100644 index a3a0417c..00000000 --- a/thirdparty/dist/django_debug_toolbar-5.0.1-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,14 +0,0 @@ -about_resource: django_debug_toolbar-5.0.1-py3-none-any.whl -name: django-debug-toolbar -version: 5.0.1 -download_url: https://files.pythonhosted.org/packages/36/f7/3b406dc871d88c938d2bd967e65acf725b46c4e7c9add6a1f1bbaff9bdcf/django_debug_toolbar-5.0.1-py3-none-any.whl -package_url: pkg:pypi/django-debug-toolbar@5.0.1 -license_expression: bsd-new -copyright: Copyright django-debug-toolbar project contributors -attribute: yes -checksum_md5: 1bb4734a70a6b45f8729a0436564e63d -checksum_sha1: 5c208d49775bb89b7e743c30ad7d4c370fad0d97 -licenses: - - key: bsd-new - name: BSD-3-Clause - file: bsd-new.LICENSE diff --git a/thirdparty/dist/django_debug_toolbar-5.0.1-py3-none-any.whl b/thirdparty/dist/django_debug_toolbar-6.2.0-py3-none-any.whl similarity index 59% rename from thirdparty/dist/django_debug_toolbar-5.0.1-py3-none-any.whl rename to thirdparty/dist/django_debug_toolbar-6.2.0-py3-none-any.whl index 22a4e0a2..3212e26e 100644 Binary files a/thirdparty/dist/django_debug_toolbar-5.0.1-py3-none-any.whl and b/thirdparty/dist/django_debug_toolbar-6.2.0-py3-none-any.whl differ diff --git a/thirdparty/dist/django_debug_toolbar-6.2.0-py3-none-any.whl.ABOUT b/thirdparty/dist/django_debug_toolbar-6.2.0-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..34fecba3 --- /dev/null +++ b/thirdparty/dist/django_debug_toolbar-6.2.0-py3-none-any.whl.ABOUT @@ -0,0 +1,14 @@ +about_resource: django_debug_toolbar-6.2.0-py3-none-any.whl +name: django-debug-toolbar +version: 6.2.0 +download_url: https://files.pythonhosted.org/packages/88/04/e24611299a5ee0d4edfacf935b09cfb7d5d9cb653bd7b7883c3b43a6f90d/django_debug_toolbar-6.2.0-py3-none-any.whl +package_url: pkg:pypi/django-debug-toolbar@6.2.0 +license_expression: bsd-new +copyright: Copyright django-debug-toolbar project contributors +attribute: yes +checksum_md5: 5cb76c9000f250b297ee58875730c17d +checksum_sha1: 83f35096201654e73f19a8e5ed8bda9257a76ddc +licenses: + - key: bsd-new + name: BSD-3-Clause + file: bsd-new.LICENSE diff --git a/thirdparty/dist/django_environ-0.12.0-py2.py3-none-any.whl b/thirdparty/dist/django_environ-0.12.0-py2.py3-none-any.whl deleted file mode 100644 index 3ed1b40b..00000000 Binary files a/thirdparty/dist/django_environ-0.12.0-py2.py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/django_environ-0.12.0-py2.py3-none-any.whl.ABOUT b/thirdparty/dist/django_environ-0.12.0-py2.py3-none-any.whl.ABOUT deleted file mode 100644 index ed7566d2..00000000 --- a/thirdparty/dist/django_environ-0.12.0-py2.py3-none-any.whl.ABOUT +++ /dev/null @@ -1,14 +0,0 @@ -about_resource: django_environ-0.12.0-py2.py3-none-any.whl -name: django-environ -version: 0.12.0 -download_url: https://files.pythonhosted.org/packages/83/b3/0a3bec4ecbfee960f39b1842c2f91e4754251e0a6ed443db9fe3f666ba8f/django_environ-0.12.0-py2.py3-none-any.whl -package_url: pkg:pypi/django-environ@0.12.0 -license_expression: mit -copyright: Copyright django-environ project contributors -attribute: yes -checksum_md5: efbd8fac490598baa52a18e5664c07db -checksum_sha1: a6e6ba302cd823af6de754418244a3b00cee4525 -licenses: - - key: mit - name: MIT License - file: mit.LICENSE diff --git a/thirdparty/dist/django_environ-0.13.0-py3-none-any.whl b/thirdparty/dist/django_environ-0.13.0-py3-none-any.whl new file mode 100644 index 00000000..a8a9d7f5 Binary files /dev/null and b/thirdparty/dist/django_environ-0.13.0-py3-none-any.whl differ diff --git a/thirdparty/dist/django_environ-0.13.0-py3-none-any.whl.ABOUT b/thirdparty/dist/django_environ-0.13.0-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..64095b75 --- /dev/null +++ b/thirdparty/dist/django_environ-0.13.0-py3-none-any.whl.ABOUT @@ -0,0 +1,14 @@ +about_resource: django_environ-0.13.0-py3-none-any.whl +name: django-environ +version: 0.13.0 +download_url: https://files.pythonhosted.org/packages/c4/00/3767393ece946084e1c6830a33ffb8e39d68642e27ad5ac7d4c8bd5de866/django_environ-0.13.0-py3-none-any.whl +package_url: pkg:pypi/django-environ@0.13.0 +license_expression: mit +copyright: Copyright django-environ project contributors +attribute: yes +checksum_md5: 0d64faa55da72589e44973ea4968fe3e +checksum_sha1: 98b44b29799b9226f852f47b91d1b3dcce136149 +licenses: + - key: mit + name: MIT License + file: mit.LICENSE diff --git a/thirdparty/dist/django_filter-24.3-py3-none-any.whl b/thirdparty/dist/django_filter-25.2-py3-none-any.whl similarity index 80% rename from thirdparty/dist/django_filter-24.3-py3-none-any.whl rename to thirdparty/dist/django_filter-25.2-py3-none-any.whl index ec6812ce..40875a3d 100644 Binary files a/thirdparty/dist/django_filter-24.3-py3-none-any.whl and b/thirdparty/dist/django_filter-25.2-py3-none-any.whl differ diff --git a/thirdparty/dist/django_filter-25.2-py3-none-any.whl.ABOUT b/thirdparty/dist/django_filter-25.2-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..edf6261a --- /dev/null +++ b/thirdparty/dist/django_filter-25.2-py3-none-any.whl.ABOUT @@ -0,0 +1,14 @@ +about_resource: django_filter-25.2-py3-none-any.whl +name: django-filter +version: '25.2' +download_url: https://files.pythonhosted.org/packages/c1/40/6a02495c5658beb1f31eb09952d8aa12ef3c2a66342331ce3a35f7132439/django_filter-25.2-py3-none-any.whl +package_url: pkg:pypi/django-filter@25.2 +license_expression: bsd-new +copyright: Copyright django-filter project contributors +attribute: yes +checksum_md5: c45048f113d2281c952c3b9b253e05ae +checksum_sha1: 3c7b6ad1ef39070b64ce15f6a35bc59dd5fda858 +licenses: + - key: bsd-new + name: BSD-3-Clause + file: bsd-new.LICENSE diff --git a/thirdparty/dist/django_grappelli-4.0.1-py2.py3-none-any.whl.ABOUT b/thirdparty/dist/django_grappelli-4.0.1-py2.py3-none-any.whl.ABOUT deleted file mode 100644 index 7b0f1a5f..00000000 --- a/thirdparty/dist/django_grappelli-4.0.1-py2.py3-none-any.whl.ABOUT +++ /dev/null @@ -1,14 +0,0 @@ -about_resource: django_grappelli-4.0.1-py2.py3-none-any.whl -name: django-grappelli -version: 4.0.1 -download_url: https://files.pythonhosted.org/packages/63/30/ca6b76f0583a0d1c957ad049d5669d0fc43d7c62d727ab1e23bad2b9b8f5/django_grappelli-4.0.1-py2.py3-none-any.whl -package_url: pkg:pypi/django-grappelli@4.0.1 -license_expression: bsd-new -copyright: Copyright django-grappelli project contributors -attribute: yes -checksum_md5: 32cb6084c89dd83356f76944d9cc75f2 -checksum_sha1: 09e4372790dcfc1c2fb7fcbc9c8cb34336302af2 -licenses: - - key: bsd-new - name: BSD-3-Clause - file: bsd-new.LICENSE diff --git a/thirdparty/dist/django_grappelli-4.0.1-py2.py3-none-any.whl b/thirdparty/dist/django_grappelli-4.0.3-py2.py3-none-any.whl similarity index 93% rename from thirdparty/dist/django_grappelli-4.0.1-py2.py3-none-any.whl rename to thirdparty/dist/django_grappelli-4.0.3-py2.py3-none-any.whl index bfcf2238..7f63dc0b 100644 Binary files a/thirdparty/dist/django_grappelli-4.0.1-py2.py3-none-any.whl and b/thirdparty/dist/django_grappelli-4.0.3-py2.py3-none-any.whl differ diff --git a/thirdparty/dist/django_grappelli-4.0.3-py2.py3-none-any.whl.ABOUT b/thirdparty/dist/django_grappelli-4.0.3-py2.py3-none-any.whl.ABOUT new file mode 100644 index 00000000..80b216e2 --- /dev/null +++ b/thirdparty/dist/django_grappelli-4.0.3-py2.py3-none-any.whl.ABOUT @@ -0,0 +1,14 @@ +about_resource: django_grappelli-4.0.3-py2.py3-none-any.whl +name: django-grappelli +version: 4.0.3 +download_url: https://files.pythonhosted.org/packages/95/c2/446eaf4fe74267d32f1179d2199ee0103d5a139185d651aa506b1677816c/django_grappelli-4.0.3-py2.py3-none-any.whl +package_url: pkg:pypi/django-grappelli@4.0.3 +license_expression: bsd-new +copyright: Copyright django-grappelli project contributors +attribute: yes +checksum_md5: d2252d5d420c4db7be8e146d7129af9f +checksum_sha1: 1a27c395c1fdca133d536f951eb9ab5f0056db5d +licenses: + - key: bsd-new + name: BSD-3-Clause + file: bsd-new.LICENSE diff --git a/thirdparty/dist/django_guardian-2.4.0-py3-none-any.whl b/thirdparty/dist/django_guardian-2.4.0-py3-none-any.whl deleted file mode 100644 index 59871e0e..00000000 Binary files a/thirdparty/dist/django_guardian-2.4.0-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/django_guardian-2.4.0-py3-none-any.whl.ABOUT b/thirdparty/dist/django_guardian-2.4.0-py3-none-any.whl.ABOUT deleted file mode 100644 index fa7dcd0b..00000000 --- a/thirdparty/dist/django_guardian-2.4.0-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,20 +0,0 @@ -about_resource: django_guardian-2.4.0-py3-none-any.whl -name: django-guardian -version: 2.4.0 -download_url: https://files.pythonhosted.org/packages/a2/25/869df12e544b51f583254aadbba6c1a95e11d2d08edeb9e58dd715112db5/django_guardian-2.4.0-py3-none-any.whl -homepage_url: https://pypi.org/project/django-guardian/2.4.0/ -package_url: pkg:pypi/django-guardian@2.4.0 -license_expression: bsd-simplified AND mit -copyright: Copyright (c) Lukasz Balcerzak, Code Charm Ltd -notice_file: django_guardian-2.4.0-py3-none-any.whl.NOTICE -attribute: yes -owner: django-guardian -checksum_md5: dfe84f444671fd8769099fb6e7746a6a -checksum_sha1: ea0c714588f900d1011bb2f0aa1fe0b5e6b40710 -licenses: - - key: mit - name: MIT License - file: mit.LICENSE - - key: bsd-simplified - name: BSD-2-Clause - file: bsd-simplified.LICENSE diff --git a/thirdparty/dist/django_guardian-2.4.0-py3-none-any.whl.NOTICE b/thirdparty/dist/django_guardian-2.4.0-py3-none-any.whl.NOTICE deleted file mode 100644 index 4778bc44..00000000 --- a/thirdparty/dist/django_guardian-2.4.0-py3-none-any.whl.NOTICE +++ /dev/null @@ -1 +0,0 @@ -Released under the MIT license and BSD-2-Clause \ No newline at end of file diff --git a/thirdparty/dist/django_guardian-3.3.0-py3-none-any.whl b/thirdparty/dist/django_guardian-3.3.0-py3-none-any.whl new file mode 100644 index 00000000..8338c85b Binary files /dev/null and b/thirdparty/dist/django_guardian-3.3.0-py3-none-any.whl differ diff --git a/thirdparty/dist/django_guardian-3.3.0-py3-none-any.whl.ABOUT b/thirdparty/dist/django_guardian-3.3.0-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..74dc5ef4 --- /dev/null +++ b/thirdparty/dist/django_guardian-3.3.0-py3-none-any.whl.ABOUT @@ -0,0 +1,17 @@ +about_resource: django_guardian-3.3.0-py3-none-any.whl +name: django-guardian +version: 3.3.0 +download_url: https://files.pythonhosted.org/packages/7c/3c/6517c5e27c6f9c165f989a5884f8798d66d25ce86fe44bf8c19aa4120351/django_guardian-3.3.0-py3-none-any.whl +package_url: pkg:pypi/django-guardian@3.3.0 +license_expression: bsd-new AND bsd-simplified +copyright: Copyright django-guardian project contributors +attribute: yes +checksum_md5: 3a5aa0cf9956674901b8049581e8d758 +checksum_sha1: 097986538e202561c4957f45fb52c8d789addcb6 +licenses: + - key: bsd-simplified + name: BSD-2-Clause + file: bsd-simplified.LICENSE + - key: bsd-new + name: BSD-3-Clause + file: bsd-new.LICENSE diff --git a/thirdparty/dist/django_hcaptcha_field-1.4.0-py3-none-any.whl b/thirdparty/dist/django_hcaptcha_field-1.4.0-py3-none-any.whl deleted file mode 100644 index 98acde49..00000000 Binary files a/thirdparty/dist/django_hcaptcha_field-1.4.0-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/django_hcaptcha_field-1.4.0-py3-none-any.whl.ABOUT b/thirdparty/dist/django_hcaptcha_field-1.4.0-py3-none-any.whl.ABOUT deleted file mode 100644 index f8e110d9..00000000 --- a/thirdparty/dist/django_hcaptcha_field-1.4.0-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,17 +0,0 @@ -about_resource: django_hcaptcha_field-1.4.0-py3-none-any.whl -name: django-hcaptcha-field -version: 1.4.0 -download_url: https://files.pythonhosted.org/packages/c3/9a/977f3f63f9183fda6573743db72c433ee293e4c5551dfa007713fa7877c4/django_hcaptcha_field-1.4.0-py3-none-any.whl -homepage_url: https://pypi.org/project/django-hcaptcha-field/1.4.0/ -package_url: pkg:pypi/django-hcaptcha-field@1.4.0 -license_expression: bsd-new -copyright: Copyright (c) Ties Jan Hefting -notice_file: django_hcaptcha_field-1.4.0-py3-none-any.whl.NOTICE -attribute: yes -owner: Ties Jan Hefting -checksum_md5: f729c3e1f1eee0c0a930de000cb12c98 -checksum_sha1: c2d9932429ef14d71fdfbd7ac9a97a444a3aa690 -licenses: - - key: bsd-new - name: BSD-3-Clause - file: bsd-new.LICENSE diff --git a/thirdparty/dist/django_hcaptcha_field-1.4.0-py3-none-any.whl.NOTICE b/thirdparty/dist/django_hcaptcha_field-1.4.0-py3-none-any.whl.NOTICE deleted file mode 100644 index 69e9e076..00000000 --- a/thirdparty/dist/django_hcaptcha_field-1.4.0-py3-none-any.whl.NOTICE +++ /dev/null @@ -1,24 +0,0 @@ -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -3. Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/thirdparty/dist/django_ipware-7.0.1-py2.py3-none-any.whl b/thirdparty/dist/django_ipware-7.0.1-py2.py3-none-any.whl deleted file mode 100644 index 9268076f..00000000 Binary files a/thirdparty/dist/django_ipware-7.0.1-py2.py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/django_ipware-7.0.1-py2.py3-none-any.whl.ABOUT b/thirdparty/dist/django_ipware-7.0.1-py2.py3-none-any.whl.ABOUT deleted file mode 100644 index b0456cb2..00000000 --- a/thirdparty/dist/django_ipware-7.0.1-py2.py3-none-any.whl.ABOUT +++ /dev/null @@ -1,14 +0,0 @@ -about_resource: django_ipware-7.0.1-py2.py3-none-any.whl -name: django-ipware -version: 7.0.1 -download_url: https://files.pythonhosted.org/packages/11/33/bf539925b102d68200da5b1d3eacb8aa5d5d9a065972e8b8724d0d53bb0d/django_ipware-7.0.1-py2.py3-none-any.whl -package_url: pkg:pypi/django-ipware@7.0.1 -license_expression: mit -copyright: Copyright django-ipware project contributors -attribute: yes -checksum_md5: 484b30de32a2dfc4a1291a6a78cd5eb6 -checksum_sha1: e6e482e64ceef0964ef6a453f202956b5aba7746 -licenses: - - key: mit - name: MIT License - file: mit.LICENSE diff --git a/thirdparty/dist/django_otp-1.5.0-py3-none-any.whl b/thirdparty/dist/django_otp-1.5.0-py3-none-any.whl deleted file mode 100644 index 3e9509c4..00000000 Binary files a/thirdparty/dist/django_otp-1.5.0-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/django_otp-1.5.0-py3-none-any.whl.ABOUT b/thirdparty/dist/django_otp-1.5.0-py3-none-any.whl.ABOUT deleted file mode 100644 index 75e0ad10..00000000 --- a/thirdparty/dist/django_otp-1.5.0-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,13 +0,0 @@ -about_resource: django_otp-1.5.0-py3-none-any.whl -name: django-otp -version: 1.5.0 -download_url: https://files.pythonhosted.org/packages/1c/28/057e0021ea735e817fd2a6a8c11ab52bcc2fbf69b8653b2c8712a3b59e5a/django_otp-1.5.0-py3-none-any.whl -package_url: pkg:pypi/django-otp@1.5.0 -license_expression: unlicense -copyright: Copyright django-otp project contributors -checksum_md5: cc30c4a9e6338dde148ebac66e0afdff -checksum_sha1: d1f41649de1858a3531fb3702f03be8aedcec724 -licenses: - - key: unlicense - name: Unlicense - file: unlicense.LICENSE diff --git a/thirdparty/dist/django_otp-1.5.4-py3-none-any.whl b/thirdparty/dist/django_otp-1.5.4-py3-none-any.whl deleted file mode 100644 index e1f126ba..00000000 Binary files a/thirdparty/dist/django_otp-1.5.4-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/django_otp-1.7.0-py3-none-any.whl b/thirdparty/dist/django_otp-1.7.0-py3-none-any.whl new file mode 100644 index 00000000..0118f0da Binary files /dev/null and b/thirdparty/dist/django_otp-1.7.0-py3-none-any.whl differ diff --git a/thirdparty/dist/django_otp-1.7.0-py3-none-any.whl.ABOUT b/thirdparty/dist/django_otp-1.7.0-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..f3c482f7 --- /dev/null +++ b/thirdparty/dist/django_otp-1.7.0-py3-none-any.whl.ABOUT @@ -0,0 +1,13 @@ +about_resource: django_otp-1.7.0-py3-none-any.whl +name: django-otp +version: 1.7.0 +download_url: https://files.pythonhosted.org/packages/9c/f0/75ee6cdcf916b7c67dffa87aecdd173e4d68e456839dd53b9313e9cc9201/django_otp-1.7.0-py3-none-any.whl +package_url: pkg:pypi/django-otp@1.7.0 +license_expression: unlicense +copyright: Copyright django-otp project contributors +checksum_md5: dc77d137ff8501a161a02b3710f7b9c4 +checksum_sha1: dd03964929aaf7bc89a2b9f8a3410e96c66cd958 +licenses: + - key: unlicense + name: Unlicense + file: unlicense.LICENSE diff --git a/thirdparty/dist/django_registration-5.1.0-py3-none-any.whl b/thirdparty/dist/django_registration-5.1.0-py3-none-any.whl deleted file mode 100644 index ee2fb36f..00000000 Binary files a/thirdparty/dist/django_registration-5.1.0-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/django_rq-3.0.0-py2.py3-none-any.whl b/thirdparty/dist/django_rq-3.0.0-py2.py3-none-any.whl deleted file mode 100644 index f510a251..00000000 Binary files a/thirdparty/dist/django_rq-3.0.0-py2.py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/django_rq-3.0.0-py2.py3-none-any.whl.ABOUT b/thirdparty/dist/django_rq-3.0.0-py2.py3-none-any.whl.ABOUT deleted file mode 100644 index f083a2ee..00000000 --- a/thirdparty/dist/django_rq-3.0.0-py2.py3-none-any.whl.ABOUT +++ /dev/null @@ -1,14 +0,0 @@ -about_resource: django_rq-3.0.0-py2.py3-none-any.whl -name: django-rq -version: 3.0.0 -download_url: https://files.pythonhosted.org/packages/fe/9a/6a9cdc19805c31019021f582728de5493ef4381c391434f64af6e4b5121c/django_rq-3.0.0-py2.py3-none-any.whl -package_url: pkg:pypi/django-rq@3.0.0 -license_expression: mit -copyright: Copyright django-rq project contributors -attribute: yes -checksum_md5: 104ce37b98b5c5e574f7b1e81c6a632c -checksum_sha1: c61533e1b96a0d44e7df0d3aa8df042192753b23 -licenses: - - key: mit - name: MIT License - file: mit.LICENSE diff --git a/thirdparty/dist/django_rq-3.2.2-py3-none-any.whl b/thirdparty/dist/django_rq-3.2.2-py3-none-any.whl new file mode 100644 index 00000000..1ee73a15 Binary files /dev/null and b/thirdparty/dist/django_rq-3.2.2-py3-none-any.whl differ diff --git a/thirdparty/dist/django_rq-3.2.2-py3-none-any.whl.ABOUT b/thirdparty/dist/django_rq-3.2.2-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..80db80c3 --- /dev/null +++ b/thirdparty/dist/django_rq-3.2.2-py3-none-any.whl.ABOUT @@ -0,0 +1,17 @@ +about_resource: django_rq-3.2.2-py3-none-any.whl +name: django-rq +version: 3.2.2 +download_url: https://files.pythonhosted.org/packages/e8/dc/c6c9b193b8d66b252206e77d6cfdf93724e48ebe5e86e6d70542de9064d5/django_rq-3.2.2-py3-none-any.whl +package_url: pkg:pypi/django-rq@3.2.2 +license_expression: mit AND unknown-license-reference +copyright: Copyright django-rq project contributors +attribute: yes +checksum_md5: d7f95462e65e3d06d6434ef2601002be +checksum_sha1: 8444fa3da4151ed48765630b1f857948ae221b8a +licenses: + - key: mit + name: MIT License + file: mit.LICENSE + - key: unknown-license-reference + name: Unknown License file reference + file: unknown-license-reference.LICENSE diff --git a/thirdparty/dist/djangorestframework-3.14.0-py3-none-any.whl b/thirdparty/dist/djangorestframework-3.14.0-py3-none-any.whl deleted file mode 100644 index 56c7c1c4..00000000 Binary files a/thirdparty/dist/djangorestframework-3.14.0-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/djangorestframework-3.14.0-py3-none-any.whl.ABOUT b/thirdparty/dist/djangorestframework-3.14.0-py3-none-any.whl.ABOUT deleted file mode 100644 index 5e16b424..00000000 --- a/thirdparty/dist/djangorestframework-3.14.0-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,18 +0,0 @@ -about_resource: djangorestframework-3.14.0-py3-none-any.whl -name: djangorestframework -version: 3.14.0 -download_url: https://files.pythonhosted.org/packages/ff/4b/3b46c0914ba4b7546a758c35fdfa8e7f017fcbe7f23c878239e93623337a/djangorestframework-3.14.0-py3-none-any.whl -homepage_url: https://pypi.org/project/djangorestframework/3.14.0/ -package_url: pkg:pypi/djangorestframework@3.14.0 -license_expression: bsd-new -copyright: Copyright djangorestframework project contributors -attribute: yes -owner: Encode OSS Ltd. -owner_url: https://github.com/encode -contact: http://www.encode.io/ -checksum_md5: d0faf84d9235e15bf8bd8a2656071b16 -checksum_sha1: 3e11a86dfd03b863e338fe928b36517c624c73f3 -licenses: - - key: bsd-new - name: BSD-3-Clause - file: bsd-new.LICENSE diff --git a/thirdparty/dist/djangorestframework-3.15.0-py3-none-any.whl b/thirdparty/dist/djangorestframework-3.15.0-py3-none-any.whl deleted file mode 100644 index 3fa40a76..00000000 Binary files a/thirdparty/dist/djangorestframework-3.15.0-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/djangorestframework-3.15.2-py3-none-any.whl b/thirdparty/dist/djangorestframework-3.16.1-py3-none-any.whl similarity index 82% rename from thirdparty/dist/djangorestframework-3.15.2-py3-none-any.whl rename to thirdparty/dist/djangorestframework-3.16.1-py3-none-any.whl index 225812e7..861584b2 100644 Binary files a/thirdparty/dist/djangorestframework-3.15.2-py3-none-any.whl and b/thirdparty/dist/djangorestframework-3.16.1-py3-none-any.whl differ diff --git a/thirdparty/dist/djangorestframework-3.16.1-py3-none-any.whl.ABOUT b/thirdparty/dist/djangorestframework-3.16.1-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..654c5845 --- /dev/null +++ b/thirdparty/dist/djangorestframework-3.16.1-py3-none-any.whl.ABOUT @@ -0,0 +1,14 @@ +about_resource: djangorestframework-3.16.1-py3-none-any.whl +name: djangorestframework +version: 3.16.1 +download_url: https://files.pythonhosted.org/packages/b0/ce/bf8b9d3f415be4ac5588545b5fcdbbb841977db1c1d923f7568eeabe1689/djangorestframework-3.16.1-py3-none-any.whl +package_url: pkg:pypi/djangorestframework@3.16.1 +license_expression: bsd-new +copyright: Copyright djangorestframework project contributors +attribute: yes +checksum_md5: 1f22035a3817c0b02714719bb5e713aa +checksum_sha1: 63cdcf0a7eddf6c14a8b8d7387183294de3debdd +licenses: + - key: bsd-new + name: BSD-3-Clause + file: bsd-new.LICENSE diff --git a/thirdparty/dist/doc8-1.1.2-py3-none-any.whl b/thirdparty/dist/doc8-1.1.2-py3-none-any.whl deleted file mode 100644 index b46a12fb..00000000 Binary files a/thirdparty/dist/doc8-1.1.2-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/doc8-1.1.2-py3-none-any.whl.ABOUT b/thirdparty/dist/doc8-1.1.2-py3-none-any.whl.ABOUT deleted file mode 100644 index 6afb94ac..00000000 --- a/thirdparty/dist/doc8-1.1.2-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,15 +0,0 @@ -about_resource: doc8-1.1.2-py3-none-any.whl -name: doc8 -version: 1.1.2 -download_url: https://files.pythonhosted.org/packages/0c/f1/6ffd5d76578e98a8f21ae7216b88a7212c778f665f1a8f4f8ce6f9605da4/doc8-1.1.2-py3-none-any.whl -package_url: pkg:pypi/doc8@1.1.2 -license_expression: apache-2.0 -copyright: Copyright doc8 project contributors -attribute: yes -track_changes: yes -checksum_md5: c69cec77f491c5f8e9f2d4ed11f4a1e1 -checksum_sha1: 1e13de16be623b78284b728e72cd3d7f6abb007e -licenses: - - key: apache-2.0 - name: Apache License 2.0 - file: apache-2.0.LICENSE diff --git a/thirdparty/dist/docutils-0.21.2-py3-none-any.whl b/thirdparty/dist/docutils-0.21.2-py3-none-any.whl deleted file mode 100644 index f884bf93..00000000 Binary files a/thirdparty/dist/docutils-0.21.2-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/docutils-0.21.2-py3-none-any.whl.ABOUT b/thirdparty/dist/docutils-0.21.2-py3-none-any.whl.ABOUT deleted file mode 100644 index 6c3b7a7c..00000000 --- a/thirdparty/dist/docutils-0.21.2-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,25 +0,0 @@ -about_resource: docutils-0.21.2-py3-none-any.whl -name: docutils -version: 0.21.2 -download_url: https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl -package_url: pkg:pypi/docutils@0.21.2 -license_expression: bsd-new AND gpl-1.0-plus AND public-domain AND python -copyright: Copyright docutils project contributors -redistribute: yes -attribute: yes -track_changes: yes -checksum_md5: 73664b05aff9d030f638edfc3ffaa686 -checksum_sha1: a2120453cdb1498128183696711261dd5f328068 -licenses: - - key: public-domain - name: Public Domain - file: public-domain.LICENSE - - key: bsd-new - name: BSD-3-Clause - file: bsd-new.LICENSE - - key: python - name: Python Software Foundation License v2 - file: python.LICENSE - - key: gpl-1.0-plus - name: GNU General Public License 1.0 or later - file: gpl-1.0-plus.LICENSE diff --git a/thirdparty/dist/drf_yasg-1.21.15-py3-none-any.whl b/thirdparty/dist/drf_yasg-1.21.15-py3-none-any.whl new file mode 100644 index 00000000..14f8db3e Binary files /dev/null and b/thirdparty/dist/drf_yasg-1.21.15-py3-none-any.whl differ diff --git a/thirdparty/dist/drf_yasg-1.21.15-py3-none-any.whl.ABOUT b/thirdparty/dist/drf_yasg-1.21.15-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..8b0db96a --- /dev/null +++ b/thirdparty/dist/drf_yasg-1.21.15-py3-none-any.whl.ABOUT @@ -0,0 +1,17 @@ +about_resource: drf_yasg-1.21.15-py3-none-any.whl +name: drf-yasg +version: 1.21.15 +download_url: https://files.pythonhosted.org/packages/f9/a4/400b0565cf25395f1d5e1a24e5d18dab8f4199e4212174341fea6f05747c/drf_yasg-1.21.15-py3-none-any.whl +package_url: pkg:pypi/drf-yasg@1.21.15 +license_expression: bsd-new AND unknown-license-reference +copyright: Copyright drf-yasg project contributors +attribute: yes +checksum_md5: fb6c6a1ee4c91f93a1f3da7bb3463cbd +checksum_sha1: 4d56a7c3e6447def2ccc4ca5cbaae230a832ceb7 +licenses: + - key: bsd-new + name: BSD-3-Clause + file: bsd-new.LICENSE + - key: unknown-license-reference + name: Unknown License file reference + file: unknown-license-reference.LICENSE diff --git a/thirdparty/dist/fakeredis-2.27.0-py3-none-any.whl b/thirdparty/dist/fakeredis-2.27.0-py3-none-any.whl deleted file mode 100644 index c9b6d9e3..00000000 Binary files a/thirdparty/dist/fakeredis-2.27.0-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/fakeredis-2.27.0-py3-none-any.whl.ABOUT b/thirdparty/dist/fakeredis-2.27.0-py3-none-any.whl.ABOUT deleted file mode 100644 index 79952252..00000000 --- a/thirdparty/dist/fakeredis-2.27.0-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,14 +0,0 @@ -about_resource: fakeredis-2.27.0-py3-none-any.whl -name: fakeredis -version: 2.27.0 -download_url: https://files.pythonhosted.org/packages/d9/98/4cecb38cb9345f8dcfd5c274f43e47f1070a5c2328d48dc57611b927fc47/fakeredis-2.27.0-py3-none-any.whl -package_url: pkg:pypi/fakeredis@2.27.0 -license_expression: bsd-new -copyright: Copyright fakeredis project contributors -attribute: yes -checksum_md5: 6d9efe02487c633f31b268432402522c -checksum_sha1: 917ae36eb8abdcb4573fe4549f65b278b0d80aad -licenses: - - key: bsd-new - name: BSD-3-Clause - file: bsd-new.LICENSE diff --git a/thirdparty/dist/fakeredis-2.34.1-py3-none-any.whl b/thirdparty/dist/fakeredis-2.34.1-py3-none-any.whl new file mode 100644 index 00000000..fcdc8962 Binary files /dev/null and b/thirdparty/dist/fakeredis-2.34.1-py3-none-any.whl differ diff --git a/thirdparty/dist/fakeredis-2.34.1-py3-none-any.whl.ABOUT b/thirdparty/dist/fakeredis-2.34.1-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..f1bd22b1 --- /dev/null +++ b/thirdparty/dist/fakeredis-2.34.1-py3-none-any.whl.ABOUT @@ -0,0 +1,14 @@ +about_resource: fakeredis-2.34.1-py3-none-any.whl +name: fakeredis +version: 2.34.1 +download_url: https://files.pythonhosted.org/packages/49/b5/82f89307d0d769cd9bf46a54fb9136be08e4e57c5570ae421db4c9a2ba62/fakeredis-2.34.1-py3-none-any.whl +package_url: pkg:pypi/fakeredis@2.34.1 +license_expression: bsd-new +copyright: Copyright fakeredis project contributors +attribute: yes +checksum_md5: 6ca73d3e3a1186f7855b833d3ac7d38e +checksum_sha1: fa1b74db0fe5bd4e03066c05a294c990463a8e5e +licenses: + - key: bsd-new + name: BSD-3-Clause + file: bsd-new.LICENSE diff --git a/thirdparty/dist/freezegun-1.5.1-py3-none-any.whl b/thirdparty/dist/freezegun-1.5.1-py3-none-any.whl deleted file mode 100644 index 4955690d..00000000 Binary files a/thirdparty/dist/freezegun-1.5.1-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/funcparserlib-0.3.6.tar.gz b/thirdparty/dist/funcparserlib-0.3.6.tar.gz deleted file mode 100644 index fd54200a..00000000 Binary files a/thirdparty/dist/funcparserlib-0.3.6.tar.gz and /dev/null differ diff --git a/thirdparty/dist/funcparserlib-0.3.6.tar.gz.ABOUT b/thirdparty/dist/funcparserlib-0.3.6.tar.gz.ABOUT deleted file mode 100644 index a37e6be9..00000000 --- a/thirdparty/dist/funcparserlib-0.3.6.tar.gz.ABOUT +++ /dev/null @@ -1,20 +0,0 @@ -about_resource: funcparserlib-0.3.6.tar.gz -name: funcparserlib -version: 0.3.6 -download_url: https://pypi.python.org/packages/source/f/funcparserlib/funcparserlib-0.3.6.tar.gz#md5=3aba546bdad5d0826596910551ce37c0 -description: Recursive descent parsing library for Python based on functional combinators. (Description - from project page) -homepage_url: http://code.google.com/p/funcparserlib/ -package_url: pkg:pypi/funcparserlib@0.3.6 -license_expression: mit -copyright: Copyright 2008/2011 Andrey Vlasovskikh -notice_file: funcparserlib-0.3.6.tar.gz.NOTICE -notice_url: https://github.com/vlasovskikh/funcparserlib/blob/master/LICENSE -attribute: yes -owner: Andrey Vlasovskikh -checksum_md5: 3aba546bdad5d0826596910551ce37c0 -checksum_sha1: 6db0e9e78dcddd993cdd488945b040934a361f03 -licenses: - - key: mit - name: MIT License - file: mit.LICENSE diff --git a/thirdparty/dist/funcparserlib-0.3.6.tar.gz.NOTICE b/thirdparty/dist/funcparserlib-0.3.6.tar.gz.NOTICE deleted file mode 100644 index 012128de..00000000 --- a/thirdparty/dist/funcparserlib-0.3.6.tar.gz.NOTICE +++ /dev/null @@ -1,18 +0,0 @@ -Copyright © 2009/2013 Andrey Vlasovskikh - -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. \ No newline at end of file diff --git a/thirdparty/dist/funcparserlib-1.0.1-py2.py3-none-any.whl b/thirdparty/dist/funcparserlib-1.0.1-py2.py3-none-any.whl deleted file mode 100644 index f3f31b4f..00000000 Binary files a/thirdparty/dist/funcparserlib-1.0.1-py2.py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/gitpython-3.1.46-py3-none-any.whl b/thirdparty/dist/gitpython-3.1.46-py3-none-any.whl new file mode 100644 index 00000000..88bc1550 Binary files /dev/null and b/thirdparty/dist/gitpython-3.1.46-py3-none-any.whl differ diff --git a/thirdparty/dist/gitpython-3.1.46-py3-none-any.whl.ABOUT b/thirdparty/dist/gitpython-3.1.46-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..b43a16c1 --- /dev/null +++ b/thirdparty/dist/gitpython-3.1.46-py3-none-any.whl.ABOUT @@ -0,0 +1,14 @@ +about_resource: gitpython-3.1.46-py3-none-any.whl +name: gitpython +version: 3.1.46 +download_url: https://files.pythonhosted.org/packages/6a/09/e21df6aef1e1ffc0c816f0522ddc3f6dcded766c3261813131c78a704470/gitpython-3.1.46-py3-none-any.whl +package_url: pkg:pypi/gitpython@3.1.46 +license_expression: bsd-new +copyright: Copyright gitpython project contributors +attribute: yes +checksum_md5: 0b9ccbb78ace1a13175d9fbe935a295f +checksum_sha1: f1bfc68d7e6f2697425a4f25a66dbaa21e872084 +licenses: + - key: bsd-new + name: BSD-3-Clause + file: bsd-new.LICENSE diff --git a/thirdparty/dist/gunicorn-22.0.0-py3-none-any.whl b/thirdparty/dist/gunicorn-22.0.0-py3-none-any.whl deleted file mode 100644 index a8a7404c..00000000 Binary files a/thirdparty/dist/gunicorn-22.0.0-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/gunicorn-22.0.0-py3-none-any.whl.ABOUT b/thirdparty/dist/gunicorn-22.0.0-py3-none-any.whl.ABOUT deleted file mode 100644 index 09365ae3..00000000 --- a/thirdparty/dist/gunicorn-22.0.0-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,14 +0,0 @@ -about_resource: gunicorn-22.0.0-py3-none-any.whl -name: gunicorn -version: 22.0.0 -download_url: https://files.pythonhosted.org/packages/29/97/6d610ae77b5633d24b69c2ff1ac3044e0e565ecbd1ec188f02c45073054c/gunicorn-22.0.0-py3-none-any.whl -package_url: pkg:pypi/gunicorn@22.0.0 -license_expression: mit -copyright: Copyright gunicorn project contributors -attribute: yes -checksum_md5: 273b5018f409cd4854576d1012da90b6 -checksum_sha1: 9e5245b32e7820b36d5872247967aaf9da814dc2 -licenses: - - key: mit - name: MIT License - file: mit.LICENSE diff --git a/thirdparty/dist/gunicorn-23.0.0-py3-none-any.whl b/thirdparty/dist/gunicorn-23.0.0-py3-none-any.whl deleted file mode 100644 index 86d6da8c..00000000 Binary files a/thirdparty/dist/gunicorn-23.0.0-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/gunicorn-25.1.0-py3-none-any.whl b/thirdparty/dist/gunicorn-25.1.0-py3-none-any.whl new file mode 100644 index 00000000..4063a4e5 Binary files /dev/null and b/thirdparty/dist/gunicorn-25.1.0-py3-none-any.whl differ diff --git a/thirdparty/dist/gunicorn-25.1.0-py3-none-any.whl.ABOUT b/thirdparty/dist/gunicorn-25.1.0-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..51a39af3 --- /dev/null +++ b/thirdparty/dist/gunicorn-25.1.0-py3-none-any.whl.ABOUT @@ -0,0 +1,17 @@ +about_resource: gunicorn-25.1.0-py3-none-any.whl +name: gunicorn +version: 25.1.0 +download_url: https://files.pythonhosted.org/packages/da/73/4ad5b1f6a2e21cf1e85afdaad2b7b1a933985e2f5d679147a1953aaa192c/gunicorn-25.1.0-py3-none-any.whl +package_url: pkg:pypi/gunicorn@25.1.0 +license_expression: mit AND unknown-license-reference +copyright: Copyright gunicorn project contributors +attribute: yes +checksum_md5: c9bd20cb582eac270fdf099eeae5bc2e +checksum_sha1: 7d471dc7b3c36a49724ca91dc8893904251fc993 +licenses: + - key: mit + name: MIT License + file: mit.LICENSE + - key: unknown-license-reference + name: Unknown License file reference + file: unknown-license-reference.LICENSE diff --git a/thirdparty/dist/idna-3.10-py3-none-any.whl b/thirdparty/dist/idna-3.10-py3-none-any.whl deleted file mode 100644 index 52759bdd..00000000 Binary files a/thirdparty/dist/idna-3.10-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/idna-3.11-py3-none-any.whl b/thirdparty/dist/idna-3.11-py3-none-any.whl new file mode 100644 index 00000000..28f2c109 Binary files /dev/null and b/thirdparty/dist/idna-3.11-py3-none-any.whl differ diff --git a/thirdparty/dist/idna-3.11-py3-none-any.whl.ABOUT b/thirdparty/dist/idna-3.11-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..d9e3ff2b --- /dev/null +++ b/thirdparty/dist/idna-3.11-py3-none-any.whl.ABOUT @@ -0,0 +1,17 @@ +about_resource: idna-3.11-py3-none-any.whl +name: idna +version: '3.11' +download_url: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl +package_url: pkg:pypi/idna@3.11 +license_expression: bsd-new AND unknown-license-reference +copyright: Copyright idna project contributors +attribute: yes +checksum_md5: 9a707ac0a65f018883cff8e82f48314e +checksum_sha1: b656add136d8a54445605020055e18a942352fa2 +licenses: + - key: bsd-new + name: BSD-3-Clause + file: bsd-new.LICENSE + - key: unknown-license-reference + name: Unknown License file reference + file: unknown-license-reference.LICENSE diff --git a/thirdparty/dist/idna-3.7-py3-none-any.whl b/thirdparty/dist/idna-3.7-py3-none-any.whl deleted file mode 100644 index fa4c95b1..00000000 Binary files a/thirdparty/dist/idna-3.7-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/idna-3.7-py3-none-any.whl.ABOUT b/thirdparty/dist/idna-3.7-py3-none-any.whl.ABOUT deleted file mode 100644 index 47d11db7..00000000 --- a/thirdparty/dist/idna-3.7-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,14 +0,0 @@ -about_resource: idna-3.7-py3-none-any.whl -name: idna -version: '3.7' -download_url: https://files.pythonhosted.org/packages/e5/3e/741d8c82801c347547f8a2a06aa57dbb1992be9e948df2ea0eda2c8b79e8/idna-3.7-py3-none-any.whl -package_url: pkg:pypi/idna@3.7 -license_expression: bsd-new -copyright: Copyright idna project contributors -attribute: yes -checksum_md5: 6077da9f00e02686ad1bc7a3c0397edc -checksum_sha1: ccb2491074ec1b6ffda6e6c11c1c668f885ed20a -licenses: - - key: bsd-new - name: BSD-3-Clause - file: bsd-new.LICENSE diff --git a/thirdparty/dist/importlib_metadata-8.6.1-py3-none-any.whl b/thirdparty/dist/importlib_metadata-8.6.1-py3-none-any.whl deleted file mode 100644 index f057be7a..00000000 Binary files a/thirdparty/dist/importlib_metadata-8.6.1-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/inflection-0.5.1-py2.py3-none-any.whl b/thirdparty/dist/inflection-0.5.1-py2.py3-none-any.whl new file mode 100644 index 00000000..f1cc3774 Binary files /dev/null and b/thirdparty/dist/inflection-0.5.1-py2.py3-none-any.whl differ diff --git a/thirdparty/dist/inflection-0.5.1-py2.py3-none-any.whl.ABOUT b/thirdparty/dist/inflection-0.5.1-py2.py3-none-any.whl.ABOUT new file mode 100644 index 00000000..73c26aaf --- /dev/null +++ b/thirdparty/dist/inflection-0.5.1-py2.py3-none-any.whl.ABOUT @@ -0,0 +1,14 @@ +about_resource: inflection-0.5.1-py2.py3-none-any.whl +name: inflection +version: 0.5.1 +download_url: https://files.pythonhosted.org/packages/59/91/aa6bde563e0085a02a435aa99b49ef75b0a4b062635e606dab23ce18d720/inflection-0.5.1-py2.py3-none-any.whl +package_url: pkg:pypi/inflection@0.5.1 +license_expression: mit +copyright: Copyright inflection project contributors +attribute: yes +checksum_md5: 8c12f5ff8ebd8c4e5e0767cab635100f +checksum_sha1: 34b129b8a4df2b62019a83633b77525022616acd +licenses: + - key: mit + name: MIT License + file: mit.LICENSE diff --git a/thirdparty/dist/itypes-1.2.0-py2.py3-none-any.whl b/thirdparty/dist/itypes-1.2.0-py2.py3-none-any.whl deleted file mode 100644 index 9e7ac864..00000000 Binary files a/thirdparty/dist/itypes-1.2.0-py2.py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/itypes-1.2.0-py2.py3-none-any.whl.ABOUT b/thirdparty/dist/itypes-1.2.0-py2.py3-none-any.whl.ABOUT deleted file mode 100644 index 750c3cfd..00000000 --- a/thirdparty/dist/itypes-1.2.0-py2.py3-none-any.whl.ABOUT +++ /dev/null @@ -1,19 +0,0 @@ -about_resource: itypes-1.2.0-py2.py3-none-any.whl -name: itypes -version: 1.2.0 -download_url: https://files.pythonhosted.org/packages/3f/bb/3bd99c7cd34d4a123b2903e16da364f6d2078b1c3a3530a8ad105c668104/itypes-1.2.0-py2.py3-none-any.whl#sha256=03da6872ca89d29aef62773672b2d408f490f80db48b23079a4b194c86dd04c6 -description: Basic immutable container types for Python. -homepage_url: https://github.com/tomchristie/itypes -package_url: pkg:pypi/itypes@1.2.0 -license_expression: bsd-simplified -copyright: Copyright (c) Tom Christie -notice_file: itypes-1.2.0-py2.py3-none-any.whl.NOTICE -attribute: yes -owner: Tom Christie -owner_url: https://github.com/tomchristie -checksum_md5: c5fe660c8dda870f7e64896c8f324bc4 -checksum_sha1: 96cf83ac83f3788aa55caaf851416ba486cd0566 -licenses: - - key: bsd-simplified - name: BSD-2-Clause - file: bsd-simplified.LICENSE diff --git a/thirdparty/dist/itypes-1.2.0-py2.py3-none-any.whl.NOTICE b/thirdparty/dist/itypes-1.2.0-py2.py3-none-any.whl.NOTICE deleted file mode 100644 index fff3f23e..00000000 --- a/thirdparty/dist/itypes-1.2.0-py2.py3-none-any.whl.NOTICE +++ /dev/null @@ -1,9 +0,0 @@ -Metadata-Version: 2.1 -Name: itypes -Version: 1.2.0 -Summary: Simple immutable types for python. -Home-page: http://github.com/PavanTatikonda/itypes -Author: Tom Christie -Author-email: tom@tomchristie.com -License: BSD -Description: # itypes \ No newline at end of file diff --git a/thirdparty/dist/jinja2-3.1.5-py3-none-any.whl.ABOUT b/thirdparty/dist/jinja2-3.1.5-py3-none-any.whl.ABOUT deleted file mode 100644 index b2b422ad..00000000 --- a/thirdparty/dist/jinja2-3.1.5-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,14 +0,0 @@ -about_resource: jinja2-3.1.5-py3-none-any.whl -name: jinja2 -version: 3.1.5 -download_url: https://files.pythonhosted.org/packages/bd/0f/2ba5fbcd631e3e88689309dbe978c5769e883e4b84ebfe7da30b43275c5a/jinja2-3.1.5-py3-none-any.whl -package_url: pkg:pypi/jinja2@3.1.5 -license_expression: bsd-new -copyright: Copyright jinja2 project contributors -attribute: yes -checksum_md5: 837dcd1af6bb3940d8d6730977f2dcad -checksum_sha1: 4d6f9a569e41943cf569f4c3eff67d14c6586e9c -licenses: - - key: bsd-new - name: BSD-3-Clause - file: bsd-new.LICENSE diff --git a/thirdparty/dist/jinja2-3.1.5-py3-none-any.whl b/thirdparty/dist/jinja2-3.1.6-py3-none-any.whl similarity index 83% rename from thirdparty/dist/jinja2-3.1.5-py3-none-any.whl rename to thirdparty/dist/jinja2-3.1.6-py3-none-any.whl index 5f4ca3f5..5046d779 100644 Binary files a/thirdparty/dist/jinja2-3.1.5-py3-none-any.whl and b/thirdparty/dist/jinja2-3.1.6-py3-none-any.whl differ diff --git a/thirdparty/dist/jinja2-3.1.6-py3-none-any.whl.ABOUT b/thirdparty/dist/jinja2-3.1.6-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..18317de7 --- /dev/null +++ b/thirdparty/dist/jinja2-3.1.6-py3-none-any.whl.ABOUT @@ -0,0 +1,14 @@ +about_resource: jinja2-3.1.6-py3-none-any.whl +name: jinja2 +version: 3.1.6 +download_url: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl +package_url: pkg:pypi/jinja2@3.1.6 +license_expression: bsd-new +copyright: Copyright jinja2 project contributors +attribute: yes +checksum_md5: 845b37cea56edd0f4dbd949244e9d798 +checksum_sha1: 65a4983e02ace6506cc5fc4b4abd5a992da4e786 +licenses: + - key: bsd-new + name: BSD-3-Clause + file: bsd-new.LICENSE diff --git a/thirdparty/dist/jsonfield-3.1.0-py3-none-any.whl b/thirdparty/dist/jsonfield-3.1.0-py3-none-any.whl deleted file mode 100644 index 2bf2a698..00000000 Binary files a/thirdparty/dist/jsonfield-3.1.0-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/jsonfield-3.1.0-py3-none-any.whl.ABOUT b/thirdparty/dist/jsonfield-3.1.0-py3-none-any.whl.ABOUT deleted file mode 100644 index 9a4ec803..00000000 --- a/thirdparty/dist/jsonfield-3.1.0-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,22 +0,0 @@ -about_resource: jsonfield-3.1.0-py3-none-any.whl -name: jsonfield -version: 3.1.0 -download_url: https://files.pythonhosted.org/packages/7c/97/3a4805532a9c1982368fd9f37b58133419e83704744b733ccab9e9827176/jsonfield-3.1.0-py3-none-any.whl#sha256=df857811587f252b97bafba42e02805e70a398a7a47870bc6358a0308dd689ed -description: django-jsonfield is a reusable Django field that allows you to store validated - JSON in your model. -homepage_url: https://pypi.org/project/jsonfield/ -package_url: pkg:pypi/jsonfield@3.1.0 -license_expression: mit -copyright: Copyright (c) 2012-2017 Brad Jasper -notice_file: jsonfield-3.1.0-py3-none-any.whl.NOTICE -notice_url: https://github.com/dmkoch/django-jsonfield/blob/2.0.2/LICENSE -attribute: yes -owner: Brad Jasper -owner_url: https://bradjasper.com/ -contact: contact@bradjasper.com -checksum_md5: 13f1ad7433832529b9182c4bc772e389 -checksum_sha1: b0b10cd0b430e844f419c0e49b06c3ef64afd90e -licenses: - - key: mit - name: MIT License - file: mit.LICENSE diff --git a/thirdparty/dist/jsonfield-3.1.0-py3-none-any.whl.NOTICE b/thirdparty/dist/jsonfield-3.1.0-py3-none-any.whl.NOTICE deleted file mode 100644 index c46d3dd0..00000000 --- a/thirdparty/dist/jsonfield-3.1.0-py3-none-any.whl.NOTICE +++ /dev/null @@ -1,20 +0,0 @@ -Copyright (c) 2012-2017 Brad Jasper - -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. \ No newline at end of file diff --git a/thirdparty/dist/jsonfield-3.2.0-py3-none-any.whl b/thirdparty/dist/jsonfield-3.2.0-py3-none-any.whl new file mode 100644 index 00000000..77fee8c2 Binary files /dev/null and b/thirdparty/dist/jsonfield-3.2.0-py3-none-any.whl differ diff --git a/thirdparty/dist/jsonfield-3.2.0-py3-none-any.whl.ABOUT b/thirdparty/dist/jsonfield-3.2.0-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..850d8df0 --- /dev/null +++ b/thirdparty/dist/jsonfield-3.2.0-py3-none-any.whl.ABOUT @@ -0,0 +1,13 @@ +about_resource: jsonfield-3.2.0-py3-none-any.whl +name: jsonfield +version: 3.2.0 +download_url: https://files.pythonhosted.org/packages/0a/22/2e08e7b957f50e5eceefde018ce9ee88aceb5126231128d9c1cb8167c1c8/jsonfield-3.2.0-py3-none-any.whl +package_url: pkg:pypi/jsonfield@3.2.0 +license_expression: unknown-license-reference +copyright: Copyright jsonfield project contributors +checksum_md5: f96e7738372863025ae93a4f7bc4988c +checksum_sha1: 3efe9e6222051aa4d2ebde604e96c2ef761e06d5 +licenses: + - key: unknown-license-reference + name: Unknown License file reference + file: unknown-license-reference.LICENSE diff --git a/thirdparty/dist/jsonschema-4.23.0-py3-none-any.whl b/thirdparty/dist/jsonschema-4.23.0-py3-none-any.whl deleted file mode 100644 index 4b8e7b44..00000000 Binary files a/thirdparty/dist/jsonschema-4.23.0-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/jsonschema-4.23.0-py3-none-any.whl.ABOUT b/thirdparty/dist/jsonschema-4.23.0-py3-none-any.whl.ABOUT deleted file mode 100644 index b0fd5076..00000000 --- a/thirdparty/dist/jsonschema-4.23.0-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,14 +0,0 @@ -about_resource: jsonschema-4.23.0-py3-none-any.whl -name: jsonschema -version: 4.23.0 -download_url: https://files.pythonhosted.org/packages/69/4a/4f9dbeb84e8850557c02365a0eee0649abe5eb1d84af92a25731c6c0f922/jsonschema-4.23.0-py3-none-any.whl -package_url: pkg:pypi/jsonschema@4.23.0 -license_expression: mit -copyright: Copyright jsonschema project contributors -attribute: yes -checksum_md5: be79227c146cfb34935b5bacaafb47fa -checksum_sha1: b32520738fd2046801f2b23f3e4372caecd11502 -licenses: - - key: mit - name: MIT License - file: mit.LICENSE diff --git a/thirdparty/dist/jsonschema-4.26.0-py3-none-any.whl b/thirdparty/dist/jsonschema-4.26.0-py3-none-any.whl new file mode 100644 index 00000000..2b6668dc Binary files /dev/null and b/thirdparty/dist/jsonschema-4.26.0-py3-none-any.whl differ diff --git a/thirdparty/dist/jsonschema-4.26.0-py3-none-any.whl.ABOUT b/thirdparty/dist/jsonschema-4.26.0-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..03de1bb9 --- /dev/null +++ b/thirdparty/dist/jsonschema-4.26.0-py3-none-any.whl.ABOUT @@ -0,0 +1,14 @@ +about_resource: jsonschema-4.26.0-py3-none-any.whl +name: jsonschema +version: 4.26.0 +download_url: https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl +package_url: pkg:pypi/jsonschema@4.26.0 +license_expression: mit +copyright: Copyright jsonschema project contributors +attribute: yes +checksum_md5: 2b7e04c1ca5e5c280087b9765d9f3970 +checksum_sha1: 6930315c45b54c0e68d7a3d039a533c8b8e7b3f1 +licenses: + - key: mit + name: MIT License + file: mit.LICENSE diff --git a/thirdparty/dist/jsonschema_specifications-2024.10.1-py3-none-any.whl.ABOUT b/thirdparty/dist/jsonschema_specifications-2024.10.1-py3-none-any.whl.ABOUT deleted file mode 100644 index e5996bad..00000000 --- a/thirdparty/dist/jsonschema_specifications-2024.10.1-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,14 +0,0 @@ -about_resource: jsonschema_specifications-2024.10.1-py3-none-any.whl -name: jsonschema-specifications -version: 2024.10.1 -download_url: https://files.pythonhosted.org/packages/d1/0f/8910b19ac0670a0f80ce1008e5e751c4a57e14d2c4c13a482aa6079fa9d6/jsonschema_specifications-2024.10.1-py3-none-any.whl -package_url: pkg:pypi/jsonschema-specifications@2024.10.1 -license_expression: mit -copyright: Copyright jsonschema-specifications project contributors -attribute: yes -checksum_md5: 6e624e5607663cd4845122dea29fc258 -checksum_sha1: ef511d5e9e07d1533c9ea4112c20f4fbc4519307 -licenses: - - key: mit - name: MIT License - file: mit.LICENSE diff --git a/thirdparty/dist/jsonschema_specifications-2024.10.1-py3-none-any.whl b/thirdparty/dist/jsonschema_specifications-2025.9.1-py3-none-any.whl similarity index 64% rename from thirdparty/dist/jsonschema_specifications-2024.10.1-py3-none-any.whl rename to thirdparty/dist/jsonschema_specifications-2025.9.1-py3-none-any.whl index 148d0787..e04d5d61 100644 Binary files a/thirdparty/dist/jsonschema_specifications-2024.10.1-py3-none-any.whl and b/thirdparty/dist/jsonschema_specifications-2025.9.1-py3-none-any.whl differ diff --git a/thirdparty/dist/jsonschema_specifications-2025.9.1-py3-none-any.whl.ABOUT b/thirdparty/dist/jsonschema_specifications-2025.9.1-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..1b9603e1 --- /dev/null +++ b/thirdparty/dist/jsonschema_specifications-2025.9.1-py3-none-any.whl.ABOUT @@ -0,0 +1,38 @@ +about_resource: jsonschema_specifications-2025.9.1-py3-none-any.whl +name: jsonschema-specifications +version: 2025.9.1 +download_url: https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl +description: | + The JSON Schema meta-schemas and vocabularies, exposed as a Registry + ============================= + ``jsonschema-specifications`` + ============================= + + |PyPI| |Pythons| |CI| |ReadTheDocs| + + JSON support files from the `JSON Schema Specifications `_ (metaschemas, vocabularies, etc.), packaged for runtime access from Python as a `referencing-based Schema Registry `_. + + .. |PyPI| image:: https://img.shields.io/pypi/v/jsonschema-specifications.svg + :alt: PyPI version + :target: https://pypi.org/project/jsonschema-specifications/ + + .. |Pythons| image:: https://img.shields.io/pypi/pyversions/jsonschema-specifications.svg + :alt: Supported Python versions + :target: https://pypi.org/project/jsonschema-specifications/ + + .. |CI| image:: https://github.com/python-jsonschema/jsonschema-specifications/workflows/CI/badge.svg + :alt: Build status + :target: https://github.com/python-jsonschema/jsonschema-specifications/actions?query=workflow%3ACI + + .. |ReadTheDocs| image:: https://readthedocs.org/projects/jsonschema-specifications/badge/?version=stable&style=flat + :alt: ReadTheDocs status + :target: https://jsonschema-specifications.readthedocs.io/en/stable/ +package_url: pkg:pypi/jsonschema-specifications@2025.9.1 +license_expression: mit +copyright: Copyright jsonschema-specifications project contributors +attribute: yes +checksum_md5: 3bc19f16b4b7bf78e337a39664d3d7ac +licenses: + - key: mit + name: MIT License + file: mit.LICENSE diff --git a/thirdparty/dist/lgpl-2.0.LICENSE b/thirdparty/dist/lgpl-2.0.LICENSE new file mode 100644 index 00000000..eb804db6 --- /dev/null +++ b/thirdparty/dist/lgpl-2.0.LICENSE @@ -0,0 +1,481 @@ +GNU LIBRARY GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1991 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the library GPL. It is + numbered 2 because it goes with version 2 of the ordinary GPL.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Library General Public License, applies to some +specially designated Free Software Foundation software, and to any +other libraries whose authors decide to use it. You can use it for +your libraries, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if +you distribute copies of the library, or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link a program with the library, you must provide +complete object files to the recipients so that they can relink them +with the library, after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + Our method of protecting your rights has two steps: (1) copyright +the library, and (2) offer you this license which gives you legal +permission to copy, distribute and/or modify the library. + + Also, for each distributor's protection, we want to make certain +that everyone understands that there is no warranty for this free +library. If the library is modified by someone else and passed on, we +want its recipients to know that what they have is not the original +version, so that any problems introduced by others will not reflect on +the original authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that companies distributing free +software will individually obtain patent licenses, thus in effect +transforming the program into proprietary software. To prevent this, +we have made it clear that any patent must be licensed for everyone's +free use or not licensed at all. + + Most GNU software, including some libraries, is covered by the ordinary +GNU General Public License, which was designed for utility programs. This +license, the GNU Library General Public License, applies to certain +designated libraries. This license is quite different from the ordinary +one; be sure to read it in full, and don't assume that anything in it is +the same as in the ordinary license. + + The reason we have a separate public license for some libraries is that +they blur the distinction we usually make between modifying or adding to a +program and simply using it. Linking a program with a library, without +changing the library, is in some sense simply using the library, and is +analogous to running a utility program or application program. However, in +a textual and legal sense, the linked executable is a combined work, a +derivative of the original library, and the ordinary General Public License +treats it as such. + + Because of this blurred distinction, using the ordinary General +Public License for libraries did not effectively promote software +sharing, because most developers did not use the libraries. We +concluded that weaker conditions might promote sharing better. + + However, unrestricted linking of non-free programs would deprive the +users of those programs of all benefit from the free status of the +libraries themselves. This Library General Public License is intended to +permit developers of non-free programs to use free libraries, while +preserving your freedom as a user of such programs to change the free +libraries that are incorporated in them. (We have not seen how to achieve +this as regards changes in header files, but we have achieved it as regards +changes in the actual functions of the Library.) The hope is that this +will lead to faster development of free libraries. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, while the latter only +works together with the library. + + Note that it is possible for a library to be covered by the ordinary +General Public License rather than by this special one. + + GNU LIBRARY GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library which +contains a notice placed by the copyright holder or other authorized +party saying it may be distributed under the terms of this Library +General Public License (also called "this License"). Each licensee is +addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also compile or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + c) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + d) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the source code distributed need not include anything that is normally +distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Library General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Libraries + + If you develop a new library, and you want it to be of the greatest +possible use to the public, we recommend making it free software that +everyone can redistribute and change. You can do so by permitting +redistribution under these terms (or, alternatively, under the terms of the +ordinary General Public License). + + To apply these terms, attach the following notices to the library. It is +safest to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public + License along with this library; if not, write to the Free + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +Also add information on how to contact you by electronic and paper mail. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the library, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + library `Frob' (a library for tweaking knobs) written by James Random Hacker. + + , 1 April 1990 + Ty Coon, President of Vice + +That's all there is to it! \ No newline at end of file diff --git a/thirdparty/dist/license_expression-30.4.1-py3-none-any.whl b/thirdparty/dist/license_expression-30.4.1-py3-none-any.whl deleted file mode 100644 index 5b8974cb..00000000 Binary files a/thirdparty/dist/license_expression-30.4.1-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/license_expression-30.4.4-py3-none-any.whl b/thirdparty/dist/license_expression-30.4.4-py3-none-any.whl new file mode 100644 index 00000000..707575f0 Binary files /dev/null and b/thirdparty/dist/license_expression-30.4.4-py3-none-any.whl differ diff --git a/thirdparty/dist/license_expression-30.4.1-py3-none-any.whl.ABOUT b/thirdparty/dist/license_expression-30.4.4-py3-none-any.whl.ABOUT similarity index 53% rename from thirdparty/dist/license_expression-30.4.1-py3-none-any.whl.ABOUT rename to thirdparty/dist/license_expression-30.4.4-py3-none-any.whl.ABOUT index 4a2505be..ab3ad726 100644 --- a/thirdparty/dist/license_expression-30.4.1-py3-none-any.whl.ABOUT +++ b/thirdparty/dist/license_expression-30.4.4-py3-none-any.whl.ABOUT @@ -1,14 +1,14 @@ -about_resource: license_expression-30.4.1-py3-none-any.whl +about_resource: license_expression-30.4.4-py3-none-any.whl name: license-expression -version: 30.4.1 -download_url: https://files.pythonhosted.org/packages/53/84/8a89614b2e7eeeaf0a68a4046d6cfaea4544c8619ea02595ebeec9b2bae3/license_expression-30.4.1-py3-none-any.whl -package_url: pkg:pypi/license-expression@30.4.1 +version: 30.4.4 +download_url: https://files.pythonhosted.org/packages/af/40/791891d4c0c4dab4c5e187c17261cedc26285fd41541577f900470a45a4d/license_expression-30.4.4-py3-none-any.whl +package_url: pkg:pypi/license-expression@30.4.4 license_expression: apache-2.0 AND cc-by-4.0 AND public-domain copyright: Copyright nexB Inc. and others attribute: yes track_changes: yes -checksum_md5: 965ece13e5daf4d68fd3f6a48556eaa5 -checksum_sha1: a1dfb4a5dfd8aaf1800eaaf236e8058ccda71bc8 +checksum_md5: 38cf609141d00ccd7b67ece1d02a8e5d +checksum_sha1: 07820db644fb16f981f64d117d6ae5e843d2de4f licenses: - key: apache-2.0 name: Apache License 2.0 diff --git a/thirdparty/dist/lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl b/thirdparty/dist/lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl new file mode 100644 index 00000000..eb4f4a3f Binary files /dev/null and b/thirdparty/dist/lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl differ diff --git a/thirdparty/dist/lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl b/thirdparty/dist/lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl new file mode 100644 index 00000000..9da79f6e Binary files /dev/null and b/thirdparty/dist/lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl differ diff --git a/thirdparty/dist/lxml-6.0.2.tar.gz b/thirdparty/dist/lxml-6.0.2.tar.gz new file mode 100644 index 00000000..c2dcbf42 Binary files /dev/null and b/thirdparty/dist/lxml-6.0.2.tar.gz differ diff --git a/thirdparty/dist/lxml-6.0.2.tar.gz.ABOUT b/thirdparty/dist/lxml-6.0.2.tar.gz.ABOUT new file mode 100644 index 00000000..c56970d2 --- /dev/null +++ b/thirdparty/dist/lxml-6.0.2.tar.gz.ABOUT @@ -0,0 +1,63 @@ +about_resource: lxml-6.0.2.tar.gz +name: lxml +version: 6.0.2 +download_url: https://files.pythonhosted.org/packages/aa/88/262177de60548e5a2bfc46ad28232c9e9cbde697bd94132aeb80364675cb/lxml-6.0.2.tar.gz +description: | + Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API. + lxml is a Pythonic, mature binding for the libxml2 and libxslt libraries. + It provides safe and convenient access to these libraries using the + ElementTree API. + + It extends the ElementTree API significantly to offer support for XPath, + RelaxNG, XML Schema, XSLT, C14N and much more. + + To contact the project, go to the `project home page `_ + or see our bug tracker at https://launchpad.net/lxml + + In case you want to use the current in-development version of lxml, + you can get it from the github repository at + https://github.com/lxml/lxml . Note that this requires Cython to + build the sources, see the build instructions on the project home page. + + + After an official release of a new stable series, bug fixes may become available at + https://github.com/lxml/lxml/tree/lxml-6.0 . + Running ``pip install https://github.com/lxml/lxml/archive/refs/heads/lxml-6.0.tar.gz`` + will install the unreleased branch state as soon as a maintenance branch has been established. + Note that this requires Cython to be installed at an appropriate version for the build. + + 6.0.2 (2025-09-21) + ================== + + Bugs fixed + ---------- + + * LP#2125278: Compilation with libxml2 2.15.0 failed. + Original patch by Xi Ruoyao. + + * Setting ``decompress=True`` in the parser had no effect in libxml2 2.15. + + * Binary wheels on Linux and macOS use the library version libxml2 2.14.6. + See https://gitlab.gnome.org/GNOME/libxml2/-/releases/v2.14.6 + + * Test failures in libxml2 2.15.0 were fixed. + + Other changes + ------------- + + * Binary wheels for Py3.9-3.11 on the ``riscv64`` architecture were added. + + * Error constants were updated to match libxml2 2.15.0. + + * Built using Cython 3.1.4. +homepage_url: https://lxml.de/ +package_url: pkg:pypi/lxml@6.0.2 +license_expression: bsd-new +copyright: Copyright Rick Jelliffe and Academia Sinica Computing Center +attribute: yes +checksum_md5: ac9a945976227fd854d3e9e034e52ca1 +checksum_sha1: 2b37a3d8ad8afe74b7ec616dc695ccc25a73bd97 +licenses: + - key: bsd-new + name: BSD-3-Clause + file: bsd-new.LICENSE diff --git a/thirdparty/dist/markdown-3.10.2-py3-none-any.whl b/thirdparty/dist/markdown-3.10.2-py3-none-any.whl new file mode 100644 index 00000000..cc232a1c Binary files /dev/null and b/thirdparty/dist/markdown-3.10.2-py3-none-any.whl differ diff --git a/thirdparty/dist/markdown-3.10.2-py3-none-any.whl.ABOUT b/thirdparty/dist/markdown-3.10.2-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..6c17f6a6 --- /dev/null +++ b/thirdparty/dist/markdown-3.10.2-py3-none-any.whl.ABOUT @@ -0,0 +1,17 @@ +about_resource: markdown-3.10.2-py3-none-any.whl +name: markdown +version: 3.10.2 +download_url: https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl +package_url: pkg:pypi/markdown@3.10.2 +license_expression: bsd-new AND unknown-license-reference +copyright: Copyright markdown project contributors +attribute: yes +checksum_md5: b8d54dbb01a69e80e57d72abdad42793 +checksum_sha1: 083b18be364835ca6be0be0e2a1263f219483f43 +licenses: + - key: bsd-new + name: BSD-3-Clause + file: bsd-new.LICENSE + - key: unknown-license-reference + name: Unknown License file reference + file: unknown-license-reference.LICENSE diff --git a/thirdparty/dist/markupsafe-3.0.2.tar.gz b/thirdparty/dist/markupsafe-3.0.2.tar.gz deleted file mode 100644 index 21376e1a..00000000 Binary files a/thirdparty/dist/markupsafe-3.0.2.tar.gz and /dev/null differ diff --git a/thirdparty/dist/markupsafe-3.0.2.tar.gz.ABOUT b/thirdparty/dist/markupsafe-3.0.2.tar.gz.ABOUT deleted file mode 100644 index 1cd85b89..00000000 --- a/thirdparty/dist/markupsafe-3.0.2.tar.gz.ABOUT +++ /dev/null @@ -1,56 +0,0 @@ -about_resource: markupsafe-3.0.2.tar.gz -name: markupsafe -version: 3.0.2 -download_url: https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz -description: | - Safely add untrusted strings to HTML/XML markup. - # MarkupSafe - - MarkupSafe implements a text object that escapes characters so it is - safe to use in HTML and XML. Characters that have special meanings are - replaced so that they display as the actual characters. This mitigates - injection attacks, meaning untrusted user input can safely be displayed - on a page. - - - ## Examples - - ```pycon - >>> from markupsafe import Markup, escape - - >>> # escape replaces special characters and wraps in Markup - >>> escape("") - Markup('<script>alert(document.cookie);</script>') - - >>> # wrap in Markup to mark text "safe" and prevent escaping - >>> Markup("Hello") - Markup('hello') - - >>> escape(Markup("Hello")) - Markup('hello') - - >>> # Markup is a str subclass - >>> # methods and operators escape their arguments - >>> template = Markup("Hello {name}") - >>> template.format(name='"World"') - Markup('Hello "World"') - ``` - - ## Donate - - The Pallets organization develops and supports MarkupSafe and other - popular packages. In order to grow the community of contributors and - users, and allow the maintainers to devote more time to the projects, - [please donate today][]. - - [please donate today]: https://palletsprojects.com/donate -package_url: pkg:pypi/markupsafe@3.0.2 -license_expression: bsd-new -copyright: Copyright Pallets -attribute: yes -checksum_md5: cb0071711b573b155cc8f86e1de72167 -checksum_sha1: b99c84f6c6e966a5221346989fa530afc0997884 -licenses: - - key: bsd-new - name: BSD-3-Clause - file: bsd-new.LICENSE diff --git a/thirdparty/dist/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl b/thirdparty/dist/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl new file mode 100644 index 00000000..48b691e8 Binary files /dev/null and b/thirdparty/dist/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl differ diff --git a/thirdparty/dist/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl b/thirdparty/dist/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl new file mode 100644 index 00000000..0a25ca79 Binary files /dev/null and b/thirdparty/dist/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl differ diff --git a/thirdparty/dist/markupsafe-3.0.3.tar.gz b/thirdparty/dist/markupsafe-3.0.3.tar.gz new file mode 100644 index 00000000..a8a7d8b7 Binary files /dev/null and b/thirdparty/dist/markupsafe-3.0.3.tar.gz differ diff --git a/thirdparty/dist/maturin-1.8.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl b/thirdparty/dist/maturin-1.11.5-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl similarity index 58% rename from thirdparty/dist/maturin-1.8.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl rename to thirdparty/dist/maturin-1.11.5-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl index 4580e643..803f9520 100644 Binary files a/thirdparty/dist/maturin-1.8.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl and b/thirdparty/dist/maturin-1.11.5-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl differ diff --git a/thirdparty/dist/maturin-1.11.5-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl b/thirdparty/dist/maturin-1.11.5-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl new file mode 100644 index 00000000..5856f1f8 Binary files /dev/null and b/thirdparty/dist/maturin-1.11.5-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl differ diff --git a/thirdparty/dist/maturin-1.11.5.tar.gz b/thirdparty/dist/maturin-1.11.5.tar.gz new file mode 100644 index 00000000..9e4341f0 Binary files /dev/null and b/thirdparty/dist/maturin-1.11.5.tar.gz differ diff --git a/thirdparty/dist/maturin-1.8.1.tar.gz.ABOUT b/thirdparty/dist/maturin-1.11.5.tar.gz.ABOUT similarity index 86% rename from thirdparty/dist/maturin-1.8.1.tar.gz.ABOUT rename to thirdparty/dist/maturin-1.11.5.tar.gz.ABOUT index 3b19acc5..77d1de10 100644 --- a/thirdparty/dist/maturin-1.8.1.tar.gz.ABOUT +++ b/thirdparty/dist/maturin-1.11.5.tar.gz.ABOUT @@ -1,7 +1,7 @@ -about_resource: maturin-1.8.1.tar.gz +about_resource: maturin-1.11.5.tar.gz name: maturin -version: 1.8.1 -download_url: https://files.pythonhosted.org/packages/9a/08/ccb0f917722a35ab0d758be9bb5edaf645c3a3d6170061f10d396ecd273f/maturin-1.8.1.tar.gz +version: 1.11.5 +download_url: https://files.pythonhosted.org/packages/a4/84/bfed8cc10e2d8b6656cf0f0ca6609218e6fcb45a62929f5094e1063570f7/maturin-1.11.5.tar.gz description: | Build and publish crates with pyo3, cffi and uniffi bindings as well as rust binaries as python packages # Maturin @@ -11,9 +11,7 @@ description: | [![Maturin User Guide](https://img.shields.io/badge/user-guide-brightgreen?logo=readthedocs&style=flat-square)](https://maturin.rs) [![Crates.io](https://img.shields.io/crates/v/maturin.svg?logo=rust&style=flat-square)](https://crates.io/crates/maturin) [![PyPI](https://img.shields.io/pypi/v/maturin.svg?logo=python&style=flat-square)](https://pypi.org/project/maturin) - [![Actions Status](https://github.com/PyO3/maturin/actions/workflows/test.yml/badge.svg)](https://github.com/PyO3/maturin/actions) - [![FreeBSD](https://img.shields.io/cirrus/github/PyO3/maturin/main?logo=CircleCI&style=flat-square)](https://cirrus-ci.com/github/PyO3/maturin) - [![discord server](https://img.shields.io/discord/1209263839632424990?logo=discord)](https://discord.gg/33kcChzH7f) + [![discord server](https://img.shields.io/discord/1209263839632424990?logo=discord&style=flat-square)](https://discord.gg/33kcChzH7f) Build and publish crates with [pyo3, cffi and uniffi bindings](https://maturin.rs/bindings) as well as rust binaries as python packages with minimal configuration. It supports building wheels for python 3.8+ on Windows, Linux, macOS and FreeBSD, can upload them to [pypi](https://pypi.org/) and has basic PyPy and GraalPy support. @@ -22,24 +20,26 @@ description: | ## Usage - You can either download binaries from the [latest release](https://github.com/PyO3/maturin/releases/latest) or install it with [pipx](https://pypa.github.io/pipx/): + You can either download binaries from the [latest release](https://github.com/PyO3/maturin/releases/latest) or install it with [pipx](https://pypa.github.io/pipx/) or [uv](https://github.com/astral-sh/uv): ```shell + # pipx pipx install maturin + # uv + uv tool install maturin ``` > [!NOTE] > > `pip install maturin` should also work if you don't want to use pipx. - There are four main commands: + There are three main commands: - `maturin new` creates a new cargo project with maturin configured. - - `maturin publish` builds the crate into python packages and publishes them to pypi. - - `maturin build` builds the wheels and stores them in a folder (`target/wheels` by default), but doesn't upload them. It's possible to upload those with [twine](https://github.com/pypa/twine) or `maturin upload`. + - `maturin build` builds the wheels and stores them in a folder (`target/wheels` by default), but doesn't upload them. It's recommended to publish packages with [uv](https://github.com/astral-sh/uv) using `uv publish`. - `maturin develop` builds the crate and installs it as a python module directly in the current virtualenv. Note that while `maturin develop` is faster, it doesn't support all the feature that running `pip install` after `maturin build` supports. - maturin doesn't need extra configuration files and doesn't clash with an existing setuptools-rust or milksnake configuration. + maturin doesn't need extra configuration files and doesn't clash with an existing setuptools-rust configuration. You can even integrate it with testing tools such as [tox](https://tox.readthedocs.io/en/latest/). There are examples for the different bindings in the `test-crates` folder. @@ -60,7 +60,7 @@ description: | When you publish a package to be installable with `pip install`, you upload it to [pypi](https://pypi.org/), the official package repository. For testing, you can use [test pypi](https://test.pypi.org/) instead, which you can use with `pip install --index-url https://test.pypi.org/simple/`. - Note that for publishing for linux, [you need to use the manylinux docker container](#manylinux-and-auditwheel), while for publishing from your repository you can use the [PyO3/maturin-action github action](https://github.com/PyO3/maturin-action). + Note that for [publishing for linux](#manylinux-and-auditwheel), you need to use the manylinux docker container or zig, while for publishing from your repository you can use the [PyO3/maturin-action](https://github.com/PyO3/maturin-action) github action. ## Mixed rust/python projects @@ -130,7 +130,7 @@ description: | ```rust #[pymodule] #[pyo3(name="_lib_name")] - fn my_lib_name(_py: Python<'_>, m: &PyModule) -> PyResult<()> { + fn my_lib_name(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; Ok(()) } @@ -146,10 +146,10 @@ description: | ```toml [project] name = "my-project" - dependencies = ["flask~=1.1.0", "toml==0.10.0"] + dependencies = ["flask~=1.1.0", "toml>=0.10.2,<0.11.0"] ``` - Pip allows adding so called console scripts, which are shell commands that execute some function in your program. You can add console scripts in a section `[project.scripts]`. + You can add so called console scripts, which are shell commands that execute some function in your program in the `[project.scripts]` section. The keys are the script names while the values are the path to the function in the format `some.module.path:class.function`, where the `class` part is optional. The function is called with no arguments. Example: ```toml @@ -210,7 +210,7 @@ description: | For portability reasons, native python modules on linux must only dynamically link a set of very few libraries which are installed basically everywhere, hence the name manylinux. The pypa offers special docker images and a tool called [auditwheel](https://github.com/pypa/auditwheel/) to ensure compliance with the [manylinux rules](https://peps.python.org/pep-0599/#the-manylinux2014-policy). - If you want to publish widely usable wheels for linux pypi, **you need to use a manylinux docker image**. + If you want to publish widely usable wheels for linux pypi, **you need to use a manylinux docker image or build with zig**. The Rust compiler since version 1.64 [requires at least glibc 2.17](https://blog.rust-lang.org/2022/08/01/Increasing-glibc-kernel-requirements.html), so you need to use at least manylinux2014. For publishing, we recommend enforcing the same manylinux version as the image with the manylinux flag, e.g. use `--manylinux 2014` if you are building in `quay.io/pypa/manylinux2014_x86_64`. @@ -234,6 +234,7 @@ description: | ## Examples + - [agg-python-bindings](https://pypi.org/project/agg-python-bindings) - A Python Library that binds to Asciinema Agg terminal record renderer and Avt terminal emulator - [ballista-python](https://github.com/apache/arrow-ballista-python) - A Python library that binds to Apache Arrow distributed query engine Ballista - [bleuscore](https://github.com/shenxiangzhuang/bleuscore) - A BLEU score calculation library, written in pure Rust - [chardetng-py](https://github.com/john-parton/chardetng-py) - Python binding for the chardetng character encoding detector. @@ -249,7 +250,10 @@ description: | - [roapi](https://github.com/roapi/roapi) - ROAPI automatically spins up read-only APIs for static datasets without requiring you to write a single line of code - [robyn](https://github.com/sansyrox/robyn) - A fast and extensible async python web server with a Rust runtime - [ruff](https://github.com/charliermarsh/ruff) - An extremely fast Python linter, written in Rust + - [rnet](https://github.com/0x676e67/rnet) - Asynchronous Python HTTP Client with Black Magic + - [rustpy-xlsxwriter](https://github.com/rahmadafandi/rustpy-xlsxwriter): A high-performance Python library for generating Excel files, utilizing the [rust_xlsxwriter](https://github.com/jmcnamara/rust_xlsxwriter) crate for efficient data handling. - [tantivy-py](https://github.com/quickwit-oss/tantivy-py) - Python bindings for Tantivy + - [tpchgen-cli](https://github.com/clflushopt/tpchgen-rs/tree/main/tpchgen-cli) - Python CLI binding for `tpchgen`, a blazing fast TPC-H benchmark data generator built in pure Rust with zero dependencies. - [watchfiles](https://github.com/samuelcolvin/watchfiles) - Simple, modern and high performance file watching and code reload in python - [wonnx](https://github.com/webonnx/wonnx/tree/master/wonnx-py) - Wonnx is a GPU-accelerated ONNX inference run-time written 100% in Rust @@ -277,13 +281,14 @@ description: | at your option. homepage_url: https://github.com/pyo3/maturin -package_url: pkg:pypi/maturin@1.8.1 -license_expression: mit OR apache-2.0 +package_url: pkg:pypi/maturin@1.11.5 +license_expression: (unknown-license-reference AND mit AND apache-2.0 AND (mit OR apache-2.0)) + AND (apache-2.0 AND mit) copyright: Copyright konstin attribute: yes track_changes: yes -checksum_md5: 6e14b8234aee912adb5f6a00f2314fb7 -checksum_sha1: 2423e481c0b8471af9a87982e1beb05286f431f3 +checksum_md5: f335b690a812734f64b00336c865a290 +checksum_sha1: 920f79f993b3891d06a285d5297a542d3a638696 licenses: - key: apache-2.0 name: Apache License 2.0 @@ -291,3 +296,6 @@ licenses: - key: mit name: MIT License file: mit.LICENSE + - key: unknown-license-reference + name: Unknown License file reference + file: unknown-license-reference.LICENSE diff --git a/thirdparty/dist/maturin-1.8.1-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl b/thirdparty/dist/maturin-1.8.1-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl deleted file mode 100644 index 2c6c6684..00000000 Binary files a/thirdparty/dist/maturin-1.8.1-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl and /dev/null differ diff --git a/thirdparty/dist/maturin-1.8.1.tar.gz b/thirdparty/dist/maturin-1.8.1.tar.gz deleted file mode 100644 index 6197caba..00000000 Binary files a/thirdparty/dist/maturin-1.8.1.tar.gz and /dev/null differ diff --git a/thirdparty/dist/mockldap-0.3.0.post1-py2.py3-none-any.whl b/thirdparty/dist/mockldap-0.3.0.post1-py2.py3-none-any.whl deleted file mode 100644 index de730ce7..00000000 Binary files a/thirdparty/dist/mockldap-0.3.0.post1-py2.py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/mockldap-0.3.0.post1-py2.py3-none-any.whl.ABOUT b/thirdparty/dist/mockldap-0.3.0.post1-py2.py3-none-any.whl.ABOUT deleted file mode 100644 index e675f3dc..00000000 --- a/thirdparty/dist/mockldap-0.3.0.post1-py2.py3-none-any.whl.ABOUT +++ /dev/null @@ -1,22 +0,0 @@ -about_resource: mockldap-0.3.0.post1-py2.py3-none-any.whl -name: mockldap -version: 0.3.0.post1 -download_url: https://files.pythonhosted.org/packages/e3/6e/1536bc788db4cbccf3f2ffb37737af5e90f163ce69858f5aa1275981ed8a/mockldap-0.3.0.post1-py2.py3-none-any.whl#sha256=cdde6a0266be9f95d83629d530888559f4458017b884a2763ea95e9ae33b38d9 -description: This project provides a mock replacement for python-ldap (pyldap on Python 3). - It's useful for any project that would like to write unit tests against LDAP code without - relying on a running LDAP server. -homepage_url: https://bitbucket.org/psagers/mockldap -package_url: pkg:pypi/mockldap@0.3.0.post1 -license_expression: bsd-new -copyright: Copyright (c) 2013, Peter Sagerson -notice_file: mockldap-0.3.0.post1-py2.py3-none-any.whl.NOTICE -notice_url: https://bitbucket.org/psagers/mockldap/src/8a83569d4b2f250fd4384e3aca5530c8baa21965/LICENSE?at=default&fileviewer=file-view-default -attribute: yes -owner: Peter Sagerson -owner_url: https://www.linkedin.com/in/psagers -checksum_md5: 8f76e4496ac575e744de01c879af9784 -checksum_sha1: d291d06cc8126cb2f8a7669f23a78f057d5b6b58 -licenses: - - key: bsd-new - name: BSD-3-Clause - file: bsd-new.LICENSE diff --git a/thirdparty/dist/mockldap-0.3.0.post1-py2.py3-none-any.whl.NOTICE b/thirdparty/dist/mockldap-0.3.0.post1-py2.py3-none-any.whl.NOTICE deleted file mode 100644 index ebbc625e..00000000 --- a/thirdparty/dist/mockldap-0.3.0.post1-py2.py3-none-any.whl.NOTICE +++ /dev/null @@ -1,23 +0,0 @@ -Copyright (c) 2013, Peter Sagerson -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -- Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -- Redistributions in binary form must reproduce the above copyright notice, this - list of conditions and the following disclaimer in the documentation and/or - other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/thirdparty/dist/model_bakery-1.10.1-py2.py3-none-any.whl b/thirdparty/dist/model_bakery-1.10.1-py2.py3-none-any.whl deleted file mode 100644 index 413d080e..00000000 Binary files a/thirdparty/dist/model_bakery-1.10.1-py2.py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/model_bakery-1.10.1-py2.py3-none-any.whl.ABOUT b/thirdparty/dist/model_bakery-1.10.1-py2.py3-none-any.whl.ABOUT deleted file mode 100644 index 21ed3dc0..00000000 --- a/thirdparty/dist/model_bakery-1.10.1-py2.py3-none-any.whl.ABOUT +++ /dev/null @@ -1,17 +0,0 @@ -about_resource: model_bakery-1.10.1-py2.py3-none-any.whl -name: model-bakery -version: 1.10.1 -download_url: https://files.pythonhosted.org/packages/af/c8/4553249a2092b3d07d44a0d0438b06f712c618bb5736fd4642a0b6758fc9/model_bakery-1.10.1-py2.py3-none-any.whl -homepage_url: https://pypi.org/project/model-bakery/1.10.1/ -package_url: pkg:pypi/model-bakery@1.10.1 -license_expression: apache-2.0 -copyright: Copyright model-bakery project contributors -attribute: yes -track_changes: yes -owner: Vanderson Mota -checksum_md5: 179a2e8c0d3ee2b8fec93ecccbd50d4a -checksum_sha1: 59dfa24bdd6d496e8b885c57cec0543ce32d033d -licenses: - - key: apache-2.0 - name: Apache License 2.0 - file: apache-2.0.LICENSE diff --git a/thirdparty/dist/model_bakery-1.23.3-py3-none-any.whl b/thirdparty/dist/model_bakery-1.23.3-py3-none-any.whl new file mode 100644 index 00000000..50416847 Binary files /dev/null and b/thirdparty/dist/model_bakery-1.23.3-py3-none-any.whl differ diff --git a/thirdparty/dist/model_bakery-1.23.3-py3-none-any.whl.ABOUT b/thirdparty/dist/model_bakery-1.23.3-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..fd826539 --- /dev/null +++ b/thirdparty/dist/model_bakery-1.23.3-py3-none-any.whl.ABOUT @@ -0,0 +1,15 @@ +about_resource: model_bakery-1.23.3-py3-none-any.whl +name: model-bakery +version: 1.23.3 +download_url: https://files.pythonhosted.org/packages/6c/70/d2c827e2fb9aee0844da298d65e6df4d42a7bf182592c116af51037904f1/model_bakery-1.23.3-py3-none-any.whl +package_url: pkg:pypi/model-bakery@1.23.3 +license_expression: apache-2.0 +copyright: Copyright model-bakery project contributors +attribute: yes +track_changes: yes +checksum_md5: 80e0c0ca7ed4d6afceda5365c6df536a +checksum_sha1: 17ac32069196ff00eb44675490c704b89c1020ab +licenses: + - key: apache-2.0 + name: Apache License 2.0 + file: apache-2.0.LICENSE diff --git a/thirdparty/dist/msgspec-0.20.0.tar.gz b/thirdparty/dist/msgspec-0.20.0.tar.gz new file mode 100644 index 00000000..884d5155 Binary files /dev/null and b/thirdparty/dist/msgspec-0.20.0.tar.gz differ diff --git a/thirdparty/dist/msgspec-0.20.0.tar.gz.ABOUT b/thirdparty/dist/msgspec-0.20.0.tar.gz.ABOUT new file mode 100644 index 00000000..b8c32f8d --- /dev/null +++ b/thirdparty/dist/msgspec-0.20.0.tar.gz.ABOUT @@ -0,0 +1,127 @@ +about_resource: msgspec-0.20.0.tar.gz +name: msgspec +version: 0.20.0 +download_url: https://files.pythonhosted.org/packages/ea/9c/bfbd12955a49180cbd234c5d29ec6f74fe641698f0cd9df154a854fc8a15/msgspec-0.20.0.tar.gz +description: | + A fast serialization and validation library, with builtin support for JSON, MessagePack, YAML, and TOML. +

        + + msgspec + +

        + +
        + + [![CI](https://github.com/jcrist/msgspec/actions/workflows/ci.yml/badge.svg)](https://github.com/jcrist/msgspec/actions/workflows/ci.yml) + [![Documentation](https://img.shields.io/badge/docs-latest-blue.svg)](https://jcristharif.com/msgspec/) + [![License](https://img.shields.io/github/license/jcrist/msgspec.svg)](https://github.com/jcrist/msgspec/blob/main/LICENSE) + [![PyPI Version](https://img.shields.io/pypi/v/msgspec.svg)](https://pypi.org/project/msgspec/) + [![Conda Version](https://img.shields.io/conda/vn/conda-forge/msgspec.svg)](https://anaconda.org/conda-forge/msgspec) + [![Code Coverage](https://codecov.io/gh/jcrist/msgspec/branch/main/graph/badge.svg)](https://app.codecov.io/gh/jcrist/msgspec) + +
        + + `msgspec` is a *fast* serialization and validation library, with builtin + support for [JSON](https://json.org), [MessagePack](https://msgpack.org), + [YAML](https://yaml.org), and [TOML](https://toml.io/en/). It features: + + - 🚀 **High performance encoders/decoders** for common protocols. The JSON and + MessagePack implementations regularly + [benchmark](https://jcristharif.com/msgspec/benchmarks.html) as the fastest + options for Python. + + - 🎉 **Support for a wide variety of Python types**. Additional types may be + supported through + [extensions](https://jcristharif.com/msgspec/extending.html). + + - 🔍 **Zero-cost schema validation** using familiar Python type annotations. In + [benchmarks](https://jcristharif.com/msgspec/benchmarks.html) `msgspec` + decodes *and* validates JSON faster than + [orjson](https://github.com/ijl/orjson) can decode it alone. + + - ✨ **A speedy Struct type** for representing structured data. If you already + use [dataclasses](https://docs.python.org/3/library/dataclasses.html) or + [attrs](https://www.attrs.org/en/stable/), + [structs](https://jcristharif.com/msgspec/structs.html) should feel familiar. + However, they're + [5-60x faster](https://jcristharif.com/msgspec/benchmarks.html#structs) + for common operations. + + All of this is included in a + [lightweight library](https://jcristharif.com/msgspec/benchmarks.html#library-size) + with no required dependencies. + + --- + + `msgspec` may be used for serialization alone, as a faster JSON or + MessagePack library. For the greatest benefit though, we recommend using + `msgspec` to handle the full serialization & validation workflow: + + **Define** your message schemas using standard Python type annotations. + + ```python + >>> import msgspec + + >>> class User(msgspec.Struct): + ... """A new type describing a User""" + ... name: str + ... groups: set[str] = set() + ... email: str | None = None + ``` + + **Encode** messages as JSON, or one of the many other supported protocols. + + ```python + >>> alice = User("alice", groups={"admin", "engineering"}) + + >>> alice + User(name='alice', groups={"admin", "engineering"}, email=None) + + >>> msg = msgspec.json.encode(alice) + + >>> msg + b'{"name":"alice","groups":["admin","engineering"],"email":null}' + ``` + + **Decode** messages back into Python objects, with optional schema validation. + + ```python + >>> msgspec.json.decode(msg, type=User) + User(name='alice', groups={"admin", "engineering"}, email=None) + + >>> msgspec.json.decode(b'{"name":"bob","groups":[123]}', type=User) + Traceback (most recent call last): + File "", line 1, in + msgspec.ValidationError: Expected `str`, got `int` - at `$.groups[0]` + ``` + + `msgspec` is designed to be as performant as possible, while retaining some of + the nicities of validation libraries like + [pydantic](https://docs.pydantic.dev/latest/). For supported types, + encoding/decoding a message with `msgspec` can be + [~10-80x faster than alternative libraries](https://jcristharif.com/msgspec/benchmarks.html). + +

        + + + +

        + + See [the documentation](https://jcristharif.com/msgspec/) for more information. + + + ## LICENSE + + New BSD. See the + [License File](https://github.com/jcrist/msgspec/blob/main/LICENSE). +homepage_url: https://jcristharif.com/msgspec/ +package_url: pkg:pypi/msgspec@0.20.0 +license_expression: bsd-new +copyright: Copyright Jim Crist-Harif +attribute: yes +checksum_md5: cbed0fb989c48199efdbe1fc8f89e1b4 +checksum_sha1: 8a9a5a04fa077c4012231e2788a638d664641f4f +licenses: + - key: bsd-new + name: BSD-3-Clause + file: bsd-new.LICENSE diff --git a/thirdparty/dist/oauthlib-3.2.2-py3-none-any.whl b/thirdparty/dist/oauthlib-3.2.2-py3-none-any.whl deleted file mode 100644 index 0beae929..00000000 Binary files a/thirdparty/dist/oauthlib-3.2.2-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/oauthlib-3.2.2-py3-none-any.whl.ABOUT b/thirdparty/dist/oauthlib-3.2.2-py3-none-any.whl.ABOUT deleted file mode 100644 index 5656152c..00000000 --- a/thirdparty/dist/oauthlib-3.2.2-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,17 +0,0 @@ -about_resource: oauthlib-3.2.2-py3-none-any.whl -name: oauthlib -version: 3.2.2 -download_url: https://files.pythonhosted.org/packages/7e/80/cab10959dc1faead58dc8384a781dfbf93cb4d33d50988f7a69f1b7c9bbe/oauthlib-3.2.2-py3-none-any.whl -homepage_url: https://pypi.org/project/oauthlib/3.2.2/ -package_url: pkg:pypi/oauthlib@3.2.2 -license_expression: bsd-new -copyright: Copyright oauthlib project contributors -attribute: yes -owner: OAuthlib -owner_url: https://github.com/oauthlib -checksum_md5: a9126e7541baee7da8bf1ad3f216c3cd -checksum_sha1: 5f0ac029e5fb17011960e697f97312432050f968 -licenses: - - key: bsd-new - name: BSD-3-Clause - file: bsd-new.LICENSE diff --git a/thirdparty/dist/oauthlib-3.3.1-py3-none-any.whl b/thirdparty/dist/oauthlib-3.3.1-py3-none-any.whl new file mode 100644 index 00000000..7b59307f Binary files /dev/null and b/thirdparty/dist/oauthlib-3.3.1-py3-none-any.whl differ diff --git a/thirdparty/dist/oauthlib-3.3.1-py3-none-any.whl.ABOUT b/thirdparty/dist/oauthlib-3.3.1-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..4b79e44a --- /dev/null +++ b/thirdparty/dist/oauthlib-3.3.1-py3-none-any.whl.ABOUT @@ -0,0 +1,14 @@ +about_resource: oauthlib-3.3.1-py3-none-any.whl +name: oauthlib +version: 3.3.1 +download_url: https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl +package_url: pkg:pypi/oauthlib@3.3.1 +license_expression: bsd-new +copyright: Copyright oauthlib project contributors +attribute: yes +checksum_md5: 400a8defd59fe85effc0b94faeedacc7 +checksum_sha1: 1a8a280f2d0ccc0d4a6d5a70cc2f32f531ca3ed9 +licenses: + - key: bsd-new + name: BSD-3-Clause + file: bsd-new.LICENSE diff --git a/thirdparty/dist/odfdo-3.22.0-py3-none-any.whl b/thirdparty/dist/odfdo-3.22.0-py3-none-any.whl new file mode 100644 index 00000000..6997173d Binary files /dev/null and b/thirdparty/dist/odfdo-3.22.0-py3-none-any.whl differ diff --git a/thirdparty/dist/odfdo-3.22.0-py3-none-any.whl.ABOUT b/thirdparty/dist/odfdo-3.22.0-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..1bde494d --- /dev/null +++ b/thirdparty/dist/odfdo-3.22.0-py3-none-any.whl.ABOUT @@ -0,0 +1,15 @@ +about_resource: odfdo-3.22.0-py3-none-any.whl +name: odfdo +version: 3.22.0 +download_url: https://files.pythonhosted.org/packages/7a/d7/07f934ea0bee8f802a4df415191b90c3e9d266c369e9b21beab66d2c0c7c/odfdo-3.22.0-py3-none-any.whl +package_url: pkg:pypi/odfdo@3.22.0 +license_expression: apache-2.0 +copyright: Copyright odfdo project contributors +attribute: yes +track_changes: yes +checksum_md5: 650cd7ff3d93ed99539a701740a939c0 +checksum_sha1: 4ecf35dc60d24d276af1c1c8cf8fe38e8d5aae3e +licenses: + - key: apache-2.0 + name: Apache License 2.0 + file: apache-2.0.LICENSE diff --git a/thirdparty/dist/openpyxl-3.1.2-py2.py3-none-any.whl b/thirdparty/dist/openpyxl-3.1.2-py2.py3-none-any.whl deleted file mode 100644 index 0d06d807..00000000 Binary files a/thirdparty/dist/openpyxl-3.1.2-py2.py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/openpyxl-3.1.2-py2.py3-none-any.whl.ABOUT b/thirdparty/dist/openpyxl-3.1.2-py2.py3-none-any.whl.ABOUT deleted file mode 100644 index afe6c962..00000000 --- a/thirdparty/dist/openpyxl-3.1.2-py2.py3-none-any.whl.ABOUT +++ /dev/null @@ -1,17 +0,0 @@ -about_resource: openpyxl-3.1.2-py2.py3-none-any.whl -name: openpyxl -version: 3.1.2 -download_url: https://files.pythonhosted.org/packages/6a/94/a59521de836ef0da54aaf50da6c4da8fb4072fb3053fa71f052fd9399e7a/openpyxl-3.1.2-py2.py3-none-any.whl -homepage_url: https://pypi.org/project/openpyxl/3.1.2/ -package_url: pkg:pypi/openpyxl@3.1.2 -license_expression: mit -copyright: Copyright openpyxl project contributors -attribute: yes -owner: openpyxl Project -owner_url: https://pypi.org/project/openpyxl/ -checksum_md5: 797657015056d50de2bc003d8a9379c2 -checksum_sha1: 0cef7ef3dffea44e711b3a08dccf812d300517d9 -licenses: - - key: mit - name: MIT License - file: mit.LICENSE diff --git a/thirdparty/dist/openpyxl-3.1.5-py2.py3-none-any.whl.ABOUT b/thirdparty/dist/openpyxl-3.1.5-py2.py3-none-any.whl.ABOUT new file mode 100644 index 00000000..05ceda33 --- /dev/null +++ b/thirdparty/dist/openpyxl-3.1.5-py2.py3-none-any.whl.ABOUT @@ -0,0 +1,14 @@ +about_resource: openpyxl-3.1.5-py2.py3-none-any.whl +name: openpyxl +version: 3.1.5 +download_url: https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl +package_url: pkg:pypi/openpyxl@3.1.5 +license_expression: mit +copyright: Copyright openpyxl project contributors +attribute: yes +checksum_md5: 05a1b34e9893bab14dd70c7645c4ddcb +checksum_sha1: 36fc73575c36117615e27d7443458b1d02d593c7 +licenses: + - key: mit + name: MIT License + file: mit.LICENSE diff --git a/thirdparty/dist/packageurl_python-0.16.0-py3-none-any.whl b/thirdparty/dist/packageurl_python-0.16.0-py3-none-any.whl deleted file mode 100644 index 2c352ac1..00000000 Binary files a/thirdparty/dist/packageurl_python-0.16.0-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/packageurl_python-0.16.0-py3-none-any.whl.ABOUT b/thirdparty/dist/packageurl_python-0.16.0-py3-none-any.whl.ABOUT deleted file mode 100644 index f1a74add..00000000 --- a/thirdparty/dist/packageurl_python-0.16.0-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,14 +0,0 @@ -about_resource: packageurl_python-0.16.0-py3-none-any.whl -name: packageurl-python -version: 0.16.0 -download_url: https://files.pythonhosted.org/packages/c4/47/3c197fb7596a813afef2e4198d507b761aed2c7150d90be64dff9fe0ea71/packageurl_python-0.16.0-py3-none-any.whl -package_url: pkg:pypi/packageurl-python@0.16.0 -license_expression: mit -copyright: Copyright packageurl-python project contributors -attribute: yes -checksum_md5: 60714b7eab3e24b416c7e236738d9934 -checksum_sha1: 019e5bce15cb3251d780f8f58b4ec4583d1bff16 -licenses: - - key: mit - name: MIT License - file: mit.LICENSE diff --git a/thirdparty/dist/packageurl_python-0.17.6-py3-none-any.whl b/thirdparty/dist/packageurl_python-0.17.6-py3-none-any.whl new file mode 100644 index 00000000..e1f4e67f Binary files /dev/null and b/thirdparty/dist/packageurl_python-0.17.6-py3-none-any.whl differ diff --git a/thirdparty/dist/packageurl_python-0.17.6-py3-none-any.whl.ABOUT b/thirdparty/dist/packageurl_python-0.17.6-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..9bfebfcc --- /dev/null +++ b/thirdparty/dist/packageurl_python-0.17.6-py3-none-any.whl.ABOUT @@ -0,0 +1,14 @@ +about_resource: packageurl_python-0.17.6-py3-none-any.whl +name: packageurl-python +version: 0.17.6 +download_url: https://files.pythonhosted.org/packages/b1/2f/c7277b7615a93f51b5fbc1eacfc1b75e8103370e786fd8ce2abf6e5c04ab/packageurl_python-0.17.6-py3-none-any.whl +package_url: pkg:pypi/packageurl-python@0.17.6 +license_expression: mit +copyright: Copyright packageurl-python project contributors +attribute: yes +checksum_md5: 61c10b06a9a6ca3b353eeb68d5d748d6 +checksum_sha1: 335c63c5b95b142a1eb3e25defe5f85ba3b4da08 +licenses: + - key: mit + name: MIT License + file: mit.LICENSE diff --git a/thirdparty/dist/packaging-24.2-py3-none-any.whl b/thirdparty/dist/packaging-24.2-py3-none-any.whl deleted file mode 100644 index b38a4a5d..00000000 Binary files a/thirdparty/dist/packaging-24.2-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/packaging-24.2-py3-none-any.whl.ABOUT b/thirdparty/dist/packaging-24.2-py3-none-any.whl.ABOUT deleted file mode 100644 index b14df332..00000000 --- a/thirdparty/dist/packaging-24.2-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,18 +0,0 @@ -about_resource: packaging-24.2-py3-none-any.whl -name: packaging -version: '24.2' -download_url: https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl -package_url: pkg:pypi/packaging@24.2 -license_expression: apache-2.0 AND bsd-new -copyright: Copyright packaging project contributors -attribute: yes -track_changes: yes -checksum_md5: 137b07612433f1ad2cd27dd8ab38ce49 -checksum_sha1: 80c56385662bb21674d3bd545a3d283b32ba2be6 -licenses: - - key: apache-2.0 - name: Apache License 2.0 - file: apache-2.0.LICENSE - - key: bsd-new - name: BSD-3-Clause - file: bsd-new.LICENSE diff --git a/thirdparty/dist/packaging-26.0-py3-none-any.whl b/thirdparty/dist/packaging-26.0-py3-none-any.whl new file mode 100644 index 00000000..2638a259 Binary files /dev/null and b/thirdparty/dist/packaging-26.0-py3-none-any.whl differ diff --git a/thirdparty/dist/packaging-26.0-py3-none-any.whl.ABOUT b/thirdparty/dist/packaging-26.0-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..84cbadc5 --- /dev/null +++ b/thirdparty/dist/packaging-26.0-py3-none-any.whl.ABOUT @@ -0,0 +1,24 @@ +about_resource: packaging-26.0-py3-none-any.whl +name: packaging +version: '26.0' +download_url: https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl +package_url: pkg:pypi/packaging@26.0 +license_expression: apache-2.0 AND bsd-new AND bsd-simplified AND unknown-license-reference +copyright: Copyright packaging project contributors +attribute: yes +track_changes: yes +checksum_md5: 6612a2b4cddb48af24b6de0de620e353 +checksum_sha1: 4a6dc6fe247bea77685d5f010a10607105910fec +licenses: + - key: apache-2.0 + name: Apache License 2.0 + file: apache-2.0.LICENSE + - key: bsd-simplified + name: BSD-2-Clause + file: bsd-simplified.LICENSE + - key: bsd-new + name: BSD-3-Clause + file: bsd-new.LICENSE + - key: unknown-license-reference + name: Unknown License file reference + file: unknown-license-reference.LICENSE diff --git a/thirdparty/dist/pbr-6.1.1-py2.py3-none-any.whl b/thirdparty/dist/pbr-6.1.1-py2.py3-none-any.whl deleted file mode 100644 index a7311274..00000000 Binary files a/thirdparty/dist/pbr-6.1.1-py2.py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/pip-25.0.1-py3-none-any.whl b/thirdparty/dist/pip-25.0.1-py3-none-any.whl deleted file mode 100644 index 8d3b0043..00000000 Binary files a/thirdparty/dist/pip-25.0.1-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/pip-25.0.1-py3-none-any.whl.ABOUT b/thirdparty/dist/pip-25.0.1-py3-none-any.whl.ABOUT deleted file mode 100644 index e4fa31cd..00000000 --- a/thirdparty/dist/pip-25.0.1-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,14 +0,0 @@ -about_resource: pip-25.0.1-py3-none-any.whl -name: pip -version: 25.0.1 -download_url: https://files.pythonhosted.org/packages/c9/bc/b7db44f5f39f9d0494071bddae6880eb645970366d0a200022a1a93d57f5/pip-25.0.1-py3-none-any.whl -package_url: pkg:pypi/pip@25.0.1 -license_expression: mit -copyright: Copyright pip project contributors -attribute: yes -checksum_md5: 99f43f22d5321305507b804a2be662c0 -checksum_sha1: 75c8684e5e766abff86e9da2b817c14979d78ad6 -licenses: - - key: mit - name: MIT License - file: mit.LICENSE diff --git a/thirdparty/dist/pip-26.0.1-py3-none-any.whl b/thirdparty/dist/pip-26.0.1-py3-none-any.whl new file mode 100644 index 00000000..580d09a9 Binary files /dev/null and b/thirdparty/dist/pip-26.0.1-py3-none-any.whl differ diff --git a/thirdparty/dist/pip-26.0.1-py3-none-any.whl.ABOUT b/thirdparty/dist/pip-26.0.1-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..d772f7d0 --- /dev/null +++ b/thirdparty/dist/pip-26.0.1-py3-none-any.whl.ABOUT @@ -0,0 +1,21 @@ +about_resource: pip-26.0.1-py3-none-any.whl +name: pip +version: 26.0.1 +download_url: https://files.pythonhosted.org/packages/de/f0/c81e05b613866b76d2d1066490adf1a3dbc4ee9d9c839961c3fc8a6997af/pip-26.0.1-py3-none-any.whl +package_url: pkg:pypi/pip@26.0.1 +license_expression: apache-2.0 AND bsd-new AND unknown-license-reference +copyright: Copyright pip project contributors +attribute: yes +track_changes: yes +checksum_md5: 081fecbf1d611c25d80665627988f83f +checksum_sha1: 836c691bd92ebf1950cdf46c17e4afc90feb3f26 +licenses: + - key: apache-2.0 + name: Apache License 2.0 + file: apache-2.0.LICENSE + - key: bsd-new + name: BSD-3-Clause + file: bsd-new.LICENSE + - key: unknown-license-reference + name: Unknown License file reference + file: unknown-license-reference.LICENSE diff --git a/thirdparty/dist/psf-2.0.LICENSE b/thirdparty/dist/psf-2.0.LICENSE new file mode 100644 index 00000000..89565571 --- /dev/null +++ b/thirdparty/dist/psf-2.0.LICENSE @@ -0,0 +1,10 @@ +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 + +1. This LICENSE AGREEMENT is between the Python Software Foundation ("PSF"), and the Individual or Organization ("Licensee") accessing and otherwise using this software ("Python") in source or binary form and its associated documentation. +2. Subject to the terms and conditions of this License Agreement, PSF hereby grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, analyze, test, perform and/or display publicly, prepare derivative works, distribute, and otherwise use Python alone or in any derivative version, provided, however, that PSF's License Agreement and PSF's notice of copyright, i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006 Python Software Foundation; All Rights Reserved" are retained in Python alone or in any derivative version prepared by Licensee. +3. In the event Licensee prepares a derivative work that is based on or incorporates Python or any part thereof, and wants to make the derivative work available to others as provided herein, then Licensee hereby agrees to include in any such work a brief summary of the changes made to Python. +4. PSF is making Python available to Licensee on an "AS IS" basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. +6. This License Agreement will automatically terminate upon a material breach of its terms and conditions. +7. Nothing in this License Agreement shall be deemed to create any relationship of agency, partnership, or joint venture between PSF and Licensee. This License Agreement does not grant permission to use PSF trademarks or trade name in a trademark sense to endorse or promote products or services of Licensee, or any third party. +8. By copying, installing or otherwise using Python, Licensee agrees to be bound by the terms and conditions of this License Agreement. \ No newline at end of file diff --git a/thirdparty/dist/psycopg-3.2.4-py3-none-any.whl b/thirdparty/dist/psycopg-3.2.4-py3-none-any.whl deleted file mode 100644 index 70df5c9b..00000000 Binary files a/thirdparty/dist/psycopg-3.2.4-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/psycopg-3.2.4-py3-none-any.whl.ABOUT b/thirdparty/dist/psycopg-3.2.4-py3-none-any.whl.ABOUT deleted file mode 100644 index c10d8a2b..00000000 --- a/thirdparty/dist/psycopg-3.2.4-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,16 +0,0 @@ -about_resource: psycopg-3.2.4-py3-none-any.whl -name: psycopg -version: 3.2.4 -download_url: https://files.pythonhosted.org/packages/40/49/15114d5f7ee68983f4e1a24d47e75334568960352a07c6f0e796e912685d/psycopg-3.2.4-py3-none-any.whl -package_url: pkg:pypi/psycopg@3.2.4 -license_expression: lgpl-3.0 -copyright: Copyright The Psycopg Team -redistribute: yes -attribute: yes -track_changes: yes -checksum_md5: e3ceb4e7e14f7d887faa2c82522d75cc -checksum_sha1: 813711aa5f16685f25b7b51b974f5580381618e3 -licenses: - - key: lgpl-3.0 - name: GNU Lesser General Public License 3.0 - file: lgpl-3.0.LICENSE diff --git a/thirdparty/dist/psycopg-3.3.3-py3-none-any.whl b/thirdparty/dist/psycopg-3.3.3-py3-none-any.whl new file mode 100644 index 00000000..d83c81be Binary files /dev/null and b/thirdparty/dist/psycopg-3.3.3-py3-none-any.whl differ diff --git a/thirdparty/dist/psycopg-3.3.3-py3-none-any.whl.ABOUT b/thirdparty/dist/psycopg-3.3.3-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..145859d0 --- /dev/null +++ b/thirdparty/dist/psycopg-3.3.3-py3-none-any.whl.ABOUT @@ -0,0 +1,19 @@ +about_resource: psycopg-3.3.3-py3-none-any.whl +name: psycopg +version: 3.3.3 +download_url: https://files.pythonhosted.org/packages/c8/5b/181e2e3becb7672b502f0ed7f16ed7352aca7c109cfb94cf3878a9186db9/psycopg-3.3.3-py3-none-any.whl +package_url: pkg:pypi/psycopg@3.3.3 +license_expression: lgpl-3.0 AND unknown-license-reference +copyright: Copyright The Psycopg Team +redistribute: yes +attribute: yes +track_changes: yes +checksum_md5: 03898ea76640473fd05a36027c16b0f2 +checksum_sha1: c47e2940a0546622b26dd9bf31a347f97658a06e +licenses: + - key: lgpl-3.0 + name: GNU Lesser General Public License 3.0 + file: lgpl-3.0.LICENSE + - key: unknown-license-reference + name: Unknown License file reference + file: unknown-license-reference.LICENSE diff --git a/thirdparty/dist/py_serializable-1.1.2-py3-none-any.whl b/thirdparty/dist/py_serializable-1.1.2-py3-none-any.whl deleted file mode 100644 index 4ccab46a..00000000 Binary files a/thirdparty/dist/py_serializable-1.1.2-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/py_serializable-2.1.0-py3-none-any.whl b/thirdparty/dist/py_serializable-2.1.0-py3-none-any.whl new file mode 100644 index 00000000..c8305589 Binary files /dev/null and b/thirdparty/dist/py_serializable-2.1.0-py3-none-any.whl differ diff --git a/thirdparty/dist/py_serializable-2.1.0-py3-none-any.whl.ABOUT b/thirdparty/dist/py_serializable-2.1.0-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..d9a90b13 --- /dev/null +++ b/thirdparty/dist/py_serializable-2.1.0-py3-none-any.whl.ABOUT @@ -0,0 +1,15 @@ +about_resource: py_serializable-2.1.0-py3-none-any.whl +name: py-serializable +version: 2.1.0 +download_url: https://files.pythonhosted.org/packages/9b/bf/7595e817906a29453ba4d99394e781b6fabe55d21f3c15d240f85dd06bb1/py_serializable-2.1.0-py3-none-any.whl +package_url: pkg:pypi/py-serializable@2.1.0 +license_expression: apache-2.0 +copyright: Copyright Paul Horton +attribute: yes +track_changes: yes +checksum_md5: 1ac75f0a28e8576d33017d4e03b6d802 +checksum_sha1: 243d4658bab38acdf6596d0b88eb59fbb4da8cd6 +licenses: + - key: apache-2.0 + name: Apache License 2.0 + file: apache-2.0.LICENSE diff --git a/thirdparty/dist/pyasn1-0.6.1-py3-none-any.whl b/thirdparty/dist/pyasn1-0.6.1-py3-none-any.whl deleted file mode 100644 index eef3fa53..00000000 Binary files a/thirdparty/dist/pyasn1-0.6.1-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/pyasn1-0.6.1-py3-none-any.whl.ABOUT b/thirdparty/dist/pyasn1-0.6.1-py3-none-any.whl.ABOUT deleted file mode 100644 index 5e7e6119..00000000 --- a/thirdparty/dist/pyasn1-0.6.1-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,17 +0,0 @@ -about_resource: pyasn1-0.6.1-py3-none-any.whl -name: pyasn1 -version: 0.6.1 -download_url: https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl -package_url: pkg:pypi/pyasn1@0.6.1 -license_expression: bsd-new AND bsd-simplified -copyright: Copyright Ilya Etingof -attribute: yes -checksum_md5: 1df61e80b7865d7693f4b9b9a433197a -checksum_sha1: df3a7fa11816f023fc31fa1669b7a9dd286a348c -licenses: - - key: bsd-simplified - name: BSD-2-Clause - file: bsd-simplified.LICENSE - - key: bsd-new - name: BSD-3-Clause - file: bsd-new.LICENSE diff --git a/thirdparty/dist/pyasn1-0.6.3-py3-none-any.whl b/thirdparty/dist/pyasn1-0.6.3-py3-none-any.whl new file mode 100644 index 00000000..74d7fbfb Binary files /dev/null and b/thirdparty/dist/pyasn1-0.6.3-py3-none-any.whl differ diff --git a/thirdparty/dist/pyasn1-0.6.3-py3-none-any.whl.ABOUT b/thirdparty/dist/pyasn1-0.6.3-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..1d6ad02a --- /dev/null +++ b/thirdparty/dist/pyasn1-0.6.3-py3-none-any.whl.ABOUT @@ -0,0 +1,14 @@ +about_resource: pyasn1-0.6.3-py3-none-any.whl +name: pyasn1 +version: 0.6.3 +download_url: https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl +package_url: pkg:pypi/pyasn1@0.6.3 +license_expression: bsd-simplified +copyright: Copyright Ilya Etingof +attribute: yes +checksum_md5: 1da50e896979cd13b73f743d482333ff +checksum_sha1: 4f8136f222687c095c633e16eced4b0a97c68743 +licenses: + - key: bsd-simplified + name: BSD-2-Clause + file: bsd-simplified.LICENSE diff --git a/thirdparty/dist/pyasn1_modules-0.4.1-py3-none-any.whl b/thirdparty/dist/pyasn1_modules-0.4.1-py3-none-any.whl deleted file mode 100644 index cddf2d6e..00000000 Binary files a/thirdparty/dist/pyasn1_modules-0.4.1-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/pyasn1_modules-0.4.1-py3-none-any.whl.ABOUT b/thirdparty/dist/pyasn1_modules-0.4.1-py3-none-any.whl.ABOUT deleted file mode 100644 index 62a4b37c..00000000 --- a/thirdparty/dist/pyasn1_modules-0.4.1-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,14 +0,0 @@ -about_resource: pyasn1_modules-0.4.1-py3-none-any.whl -name: pyasn1-modules -version: 0.4.1 -download_url: https://files.pythonhosted.org/packages/77/89/bc88a6711935ba795a679ea6ebee07e128050d6382eaa35a0a47c8032bdc/pyasn1_modules-0.4.1-py3-none-any.whl -package_url: pkg:pypi/pyasn1-modules@0.4.1 -license_expression: bsd-new -copyright: Copyright Ilya Etingof -attribute: yes -checksum_md5: 00f5f725276542ac7740532ca742e194 -checksum_sha1: 3c23b3f6a40aba58b87b72c670f996df9363e852 -licenses: - - key: bsd-new - name: BSD-3-Clause - file: bsd-new.LICENSE diff --git a/thirdparty/dist/pyasn1_modules-0.4.2-py3-none-any.whl b/thirdparty/dist/pyasn1_modules-0.4.2-py3-none-any.whl new file mode 100644 index 00000000..ecb42148 Binary files /dev/null and b/thirdparty/dist/pyasn1_modules-0.4.2-py3-none-any.whl differ diff --git a/thirdparty/dist/pyasn1_modules-0.4.2-py3-none-any.whl.ABOUT b/thirdparty/dist/pyasn1_modules-0.4.2-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..0888c4fc --- /dev/null +++ b/thirdparty/dist/pyasn1_modules-0.4.2-py3-none-any.whl.ABOUT @@ -0,0 +1,14 @@ +about_resource: pyasn1_modules-0.4.2-py3-none-any.whl +name: pyasn1-modules +version: 0.4.2 +download_url: https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl +package_url: pkg:pypi/pyasn1-modules@0.4.2 +license_expression: bsd-new +copyright: Copyright Ilya Etingof +attribute: yes +checksum_md5: 692245a75ce1da1a3443be3003d8b468 +checksum_sha1: 0f530f712e77f60dfe658064aeb501df004c3aa4 +licenses: + - key: bsd-new + name: BSD-3-Clause + file: bsd-new.LICENSE diff --git a/thirdparty/dist/pydantic-2.10.6-py3-none-any.whl b/thirdparty/dist/pydantic-2.10.6-py3-none-any.whl deleted file mode 100644 index decfdc55..00000000 Binary files a/thirdparty/dist/pydantic-2.10.6-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/pydantic-2.10.6-py3-none-any.whl.ABOUT b/thirdparty/dist/pydantic-2.10.6-py3-none-any.whl.ABOUT deleted file mode 100644 index 5315078c..00000000 --- a/thirdparty/dist/pydantic-2.10.6-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,14 +0,0 @@ -about_resource: pydantic-2.10.6-py3-none-any.whl -name: pydantic -version: 2.10.6 -download_url: https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl -package_url: pkg:pypi/pydantic@2.10.6 -license_expression: mit -copyright: Copyright pydantic project contributors -attribute: yes -checksum_md5: fc7927f675ad5f05498b8aa8afa3e29f -checksum_sha1: 5e89d881a4417af821a35b3d2d4ff20e2eba7900 -licenses: - - key: mit - name: MIT License - file: mit.LICENSE diff --git a/thirdparty/dist/pydantic-2.12.5-py3-none-any.whl b/thirdparty/dist/pydantic-2.12.5-py3-none-any.whl new file mode 100644 index 00000000..085f1062 Binary files /dev/null and b/thirdparty/dist/pydantic-2.12.5-py3-none-any.whl differ diff --git a/thirdparty/dist/pydantic-2.12.5-py3-none-any.whl.ABOUT b/thirdparty/dist/pydantic-2.12.5-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..9666fb8e --- /dev/null +++ b/thirdparty/dist/pydantic-2.12.5-py3-none-any.whl.ABOUT @@ -0,0 +1,17 @@ +about_resource: pydantic-2.12.5-py3-none-any.whl +name: pydantic +version: 2.12.5 +download_url: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl +package_url: pkg:pypi/pydantic@2.12.5 +license_expression: mit AND unknown-license-reference +copyright: Copyright pydantic project contributors +attribute: yes +checksum_md5: 0857f9b85480a4883a7ce20c8b07a025 +checksum_sha1: 1ae214c5e185c89b0ea88888b5b6d6f5badeef6d +licenses: + - key: mit + name: MIT License + file: mit.LICENSE + - key: unknown-license-reference + name: Unknown License file reference + file: unknown-license-reference.LICENSE diff --git a/thirdparty/dist/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl b/thirdparty/dist/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl deleted file mode 100644 index 02f3c6af..00000000 Binary files a/thirdparty/dist/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl and /dev/null differ diff --git a/thirdparty/dist/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl b/thirdparty/dist/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl deleted file mode 100644 index bb470f9e..00000000 Binary files a/thirdparty/dist/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl and /dev/null differ diff --git a/thirdparty/dist/pydantic_core-2.27.2.tar.gz b/thirdparty/dist/pydantic_core-2.27.2.tar.gz deleted file mode 100644 index e961f3a8..00000000 Binary files a/thirdparty/dist/pydantic_core-2.27.2.tar.gz and /dev/null differ diff --git a/thirdparty/dist/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl b/thirdparty/dist/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl new file mode 100644 index 00000000..239b87f5 Binary files /dev/null and b/thirdparty/dist/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl differ diff --git a/thirdparty/dist/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl b/thirdparty/dist/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl new file mode 100644 index 00000000..6c59f78d Binary files /dev/null and b/thirdparty/dist/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl differ diff --git a/thirdparty/dist/pydantic_core-2.41.5.tar.gz b/thirdparty/dist/pydantic_core-2.41.5.tar.gz new file mode 100644 index 00000000..f7c28e1b Binary files /dev/null and b/thirdparty/dist/pydantic_core-2.41.5.tar.gz differ diff --git a/thirdparty/dist/pydantic_core-2.41.5.tar.gz.ABOUT b/thirdparty/dist/pydantic_core-2.41.5.tar.gz.ABOUT new file mode 100644 index 00000000..c0e5a6eb --- /dev/null +++ b/thirdparty/dist/pydantic_core-2.41.5.tar.gz.ABOUT @@ -0,0 +1,159 @@ +about_resource: pydantic_core-2.41.5.tar.gz +name: pydantic-core +version: 2.41.5 +download_url: https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz +description: | + Core functionality for Pydantic validation and serialization + # pydantic-core + + [![CI](https://github.com/pydantic/pydantic-core/workflows/ci/badge.svg?event=push)](https://github.com/pydantic/pydantic-core/actions?query=event%3Apush+branch%3Amain+workflow%3Aci) + [![Coverage](https://codecov.io/gh/pydantic/pydantic-core/branch/main/graph/badge.svg)](https://codecov.io/gh/pydantic/pydantic-core) + [![pypi](https://img.shields.io/pypi/v/pydantic-core.svg)](https://pypi.python.org/pypi/pydantic-core) + [![versions](https://img.shields.io/pypi/pyversions/pydantic-core.svg)](https://github.com/pydantic/pydantic-core) + [![license](https://img.shields.io/github/license/pydantic/pydantic-core.svg)](https://github.com/pydantic/pydantic-core/blob/main/LICENSE) + + This package provides the core functionality for [pydantic](https://docs.pydantic.dev) validation and serialization. + + Pydantic-core is currently around 17x faster than pydantic V1. + See [`tests/benchmarks/`](./tests/benchmarks/) for details. + + ## Example of direct usage + + _NOTE: You should not need to use pydantic-core directly; instead, use pydantic, which in turn uses pydantic-core._ + + ```py + from pydantic_core import SchemaValidator, ValidationError + + + v = SchemaValidator( + { + 'type': 'typed-dict', + 'fields': { + 'name': { + 'type': 'typed-dict-field', + 'schema': { + 'type': 'str', + }, + }, + 'age': { + 'type': 'typed-dict-field', + 'schema': { + 'type': 'int', + 'ge': 18, + }, + }, + 'is_developer': { + 'type': 'typed-dict-field', + 'schema': { + 'type': 'default', + 'schema': {'type': 'bool'}, + 'default': True, + }, + }, + }, + } + ) + + r1 = v.validate_python({'name': 'Samuel', 'age': 35}) + assert r1 == {'name': 'Samuel', 'age': 35, 'is_developer': True} + + # pydantic-core can also validate JSON directly + r2 = v.validate_json('{"name": "Samuel", "age": 35}') + assert r1 == r2 + + try: + v.validate_python({'name': 'Samuel', 'age': 11}) + except ValidationError as e: + print(e) + """ + 1 validation error for model + age + Input should be greater than or equal to 18 + [type=greater_than_equal, context={ge: 18}, input_value=11, input_type=int] + """ + ``` + + ## Getting Started + + ### Prerequisites + + You'll need: + 1. **[Rust](https://rustup.rs/)** - Rust stable (or nightly for coverage) + 2. **[uv](https://docs.astral.sh/uv/getting-started/installation/)** - Fast Python package manager (will install Python 3.9+ automatically) + 3. **[git](https://git-scm.com/)** - For version control + 4. **[make](https://www.gnu.org/software/make/)** - For running development commands (or use `nmake` on Windows) + + ### Quick Start + + ```bash + # Clone the repository (or from your fork) + git clone git@github.com:pydantic/pydantic-core.git + cd pydantic-core + + # Install all dependencies using uv, setup pre-commit hooks, and build the development version + make install + ``` + + Verify your installation by running: + + ```bash + make + ``` + + This runs a full development cycle: formatting, building, linting, and testing + + ### Development Commands + + Run `make help` to see all available commands, or use these common ones: + + ```bash + make build-dev # to build the package during development + make build-prod # to perform an optimised build for benchmarking + make test # to run the tests + make testcov # to run the tests and generate a coverage report + make lint # to run the linter + make format # to format python and rust code + make all # to run to run build-dev + format + lint + test + ``` + + ### Useful Resources + + * [`python/pydantic_core/_pydantic_core.pyi`](./python/pydantic_core/_pydantic_core.pyi) - Python API types + * [`python/pydantic_core/core_schema.py`](./python/pydantic_core/core_schema.py) - Core schema definitions + * [`tests/`](./tests) - Comprehensive usage examples + + ## Profiling + + It's possible to profile the code using the [`flamegraph` utility from `flamegraph-rs`](https://github.com/flamegraph-rs/flamegraph). (Tested on Linux.) You can install this with `cargo install flamegraph`. + + Run `make build-profiling` to install a release build with debugging symbols included (needed for profiling). + + Once that is built, you can profile pytest benchmarks with (e.g.): + + ```bash + flamegraph -- pytest tests/benchmarks/test_micro_benchmarks.py -k test_list_of_ints_core_py --benchmark-enable + ``` + The `flamegraph` command will produce an interactive SVG at `flamegraph.svg`. + + ## Releasing + + 1. Bump package version locally. Do not just edit `Cargo.toml` on Github, you need both `Cargo.toml` and `Cargo.lock` to be updated. + 2. Make a PR for the version bump and merge it. + 3. Go to https://github.com/pydantic/pydantic-core/releases and click "Draft a new release" + 4. In the "Choose a tag" dropdown enter the new tag `v` and select "Create new tag on publish" when the option appears. + 5. Enter the release title in the form "v " + 6. Click Generate release notes button + 7. Click Publish release + 8. Go to https://github.com/pydantic/pydantic-core/actions and ensure that all build for release are done successfully. + 9. Go to https://pypi.org/project/pydantic-core/ and ensure that the latest release is published. + 10. Done 🎉 +homepage_url: https://github.com/pydantic/pydantic-core +package_url: pkg:pypi/pydantic-core@2.41.5 +license_expression: mit +copyright: Copyright Samuel Colvin +attribute: yes +checksum_md5: 54a367c4549ec48a8b3a63d38e821506 +licenses: + - key: mit + name: MIT License + file: mit.LICENSE diff --git a/thirdparty/dist/pygments-2.19.1-py3-none-any.whl b/thirdparty/dist/pygments-2.19.1-py3-none-any.whl deleted file mode 100644 index 7e3a808e..00000000 Binary files a/thirdparty/dist/pygments-2.19.1-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/pygments-2.19.1-py3-none-any.whl.ABOUT b/thirdparty/dist/pygments-2.19.1-py3-none-any.whl.ABOUT deleted file mode 100644 index ed322790..00000000 --- a/thirdparty/dist/pygments-2.19.1-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,17 +0,0 @@ -about_resource: pygments-2.19.1-py3-none-any.whl -name: pygments -version: 2.19.1 -download_url: https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl -package_url: pkg:pypi/pygments@2.19.1 -license_expression: bsd-new AND bsd-simplified -copyright: Copyright the Pygments team -attribute: yes -checksum_md5: 794747e68f6a2c85e86a8a49e4abb285 -checksum_sha1: 5e22e09a3f7049e56133740ccd6a54ebcb18cdd3 -licenses: - - key: bsd-simplified - name: BSD-2-Clause - file: bsd-simplified.LICENSE - - key: bsd-new - name: BSD-3-Clause - file: bsd-new.LICENSE diff --git a/thirdparty/dist/pyjwt-2.12.1-py3-none-any.whl b/thirdparty/dist/pyjwt-2.12.1-py3-none-any.whl new file mode 100644 index 00000000..63f17ddf Binary files /dev/null and b/thirdparty/dist/pyjwt-2.12.1-py3-none-any.whl differ diff --git a/thirdparty/dist/pyjwt-2.12.1-py3-none-any.whl.ABOUT b/thirdparty/dist/pyjwt-2.12.1-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..f4bd7261 --- /dev/null +++ b/thirdparty/dist/pyjwt-2.12.1-py3-none-any.whl.ABOUT @@ -0,0 +1,13 @@ +about_resource: pyjwt-2.12.1-py3-none-any.whl +name: pyjwt +version: 2.12.1 +download_url: https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl +package_url: pkg:pypi/pyjwt@2.12.1 +license_expression: unknown-license-reference +copyright: Copyright pyjwt project contributors +checksum_md5: 57ad6a13037c27e4b58d97747c3fea97 +checksum_sha1: 1fad5e87cfed4d960f7e1c0545c53e493cf5edd7 +licenses: + - key: unknown-license-reference + name: Unknown License file reference + file: unknown-license-reference.LICENSE diff --git a/thirdparty/dist/pyparsing-3.2.1-py3-none-any.whl b/thirdparty/dist/pyparsing-3.2.1-py3-none-any.whl deleted file mode 100644 index 1db47624..00000000 Binary files a/thirdparty/dist/pyparsing-3.2.1-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/pyparsing-3.2.1-py3-none-any.whl.ABOUT b/thirdparty/dist/pyparsing-3.2.1-py3-none-any.whl.ABOUT deleted file mode 100644 index 80aabf21..00000000 --- a/thirdparty/dist/pyparsing-3.2.1-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,14 +0,0 @@ -about_resource: pyparsing-3.2.1-py3-none-any.whl -name: pyparsing -version: 3.2.1 -download_url: https://files.pythonhosted.org/packages/1c/a7/c8a2d361bf89c0d9577c934ebb7421b25dc84bf3a8e3ac0a40aed9acc547/pyparsing-3.2.1-py3-none-any.whl -package_url: pkg:pypi/pyparsing@3.2.1 -license_expression: mit -copyright: Copyright pyparsing project contributors -attribute: yes -checksum_md5: a0dbc297f4184d68c88640be7979832c -checksum_sha1: ac4cd9fa6528558267430209b7a8e1459ac309e9 -licenses: - - key: mit - name: MIT License - file: mit.LICENSE diff --git a/thirdparty/dist/pyrsistent-0.18.1.tar.gz b/thirdparty/dist/pyrsistent-0.18.1.tar.gz deleted file mode 100644 index 5d557d1a..00000000 Binary files a/thirdparty/dist/pyrsistent-0.18.1.tar.gz and /dev/null differ diff --git a/thirdparty/dist/pyrsistent-0.20.0-py3-none-any.whl b/thirdparty/dist/pyrsistent-0.20.0-py3-none-any.whl deleted file mode 100644 index 4416bc0e..00000000 Binary files a/thirdparty/dist/pyrsistent-0.20.0-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/pyrsistent-0.20.0-py3-none-any.whl.ABOUT b/thirdparty/dist/pyrsistent-0.20.0-py3-none-any.whl.ABOUT deleted file mode 100644 index f88f42a0..00000000 --- a/thirdparty/dist/pyrsistent-0.20.0-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,14 +0,0 @@ -about_resource: pyrsistent-0.20.0-py3-none-any.whl -name: pyrsistent -version: 0.20.0 -download_url: https://files.pythonhosted.org/packages/23/88/0acd180010aaed4987c85700b7cc17f9505f3edb4e5873e4dc67f613e338/pyrsistent-0.20.0-py3-none-any.whl -package_url: pkg:pypi/pyrsistent@0.20.0 -license_expression: mit -copyright: Copyright pyrsistent project contributors -attribute: yes -checksum_md5: 2936d62f94b0e025bbdd11296c05b306 -checksum_sha1: 28ea80c386309e5e7bd9418b3b7071854e86b55a -licenses: - - key: mit - name: MIT License - file: mit.LICENSE diff --git a/thirdparty/dist/python-ldap-3.4.4.tar.gz b/thirdparty/dist/python-ldap-3.4.4.tar.gz deleted file mode 100644 index 766d2815..00000000 Binary files a/thirdparty/dist/python-ldap-3.4.4.tar.gz and /dev/null differ diff --git a/thirdparty/dist/python-ldap-3.4.4.tar.gz.ABOUT b/thirdparty/dist/python-ldap-3.4.4.tar.gz.ABOUT deleted file mode 100644 index e44542d3..00000000 --- a/thirdparty/dist/python-ldap-3.4.4.tar.gz.ABOUT +++ /dev/null @@ -1,24 +0,0 @@ -about_resource: python-ldap-3.4.4.tar.gz -name: python-ldap -version: 3.4.4 -download_url: https://files.pythonhosted.org/packages/fd/8b/1eeb4025dc1d3ac2f16678f38dec9ebdde6271c74955b72db5ce7a4dbfbd/python-ldap-3.4.4.tar.gz -description: | - Python modules for implementing LDAP clients - python-ldap: - python-ldap provides an object-oriented API to access LDAP directory servers - from Python programs. Mainly it wraps the OpenLDAP 2.x libs for that purpose. - Additionally the package contains modules for other LDAP-related stuff - (e.g. processing LDIF, LDAPURLs, LDAPv3 schema, LDAPv3 extended operations - and controls, etc.). -homepage_url: https://www.python-ldap.org/ -package_url: pkg:pypi/python-ldap@3.4.4 -license_expression: python -copyright: Copyright python-ldap project contributors -attribute: yes -track_changes: yes -checksum_md5: 6d42a0b5bdc12dca39875ea399e56501 -checksum_sha1: e2865663be86a084db58d12dbb38a5ee607bcc53 -licenses: - - key: python - name: Python Software Foundation License v2 - file: python.LICENSE diff --git a/thirdparty/dist/python-ldap.LICENSE b/thirdparty/dist/python-ldap.LICENSE new file mode 100644 index 00000000..ee9663d1 --- /dev/null +++ b/thirdparty/dist/python-ldap.LICENSE @@ -0,0 +1,10 @@ +The python-ldap package is distributed under Python-style license. + +Standard disclaimer + This software is made available by the author(s) to the public for free + and "as is". All users of this free software are solely and entirely + responsible for their own choice and use of this software for their + own purposes. By using this software, each user agrees that the + author(s) shall not be liable for damages of any kind in relation to + its use or performance. The author(s) do not warrant that this software + is fit for any purpose. \ No newline at end of file diff --git a/thirdparty/dist/python_ldap-3.4.5.tar.gz b/thirdparty/dist/python_ldap-3.4.5.tar.gz new file mode 100644 index 00000000..f1bf069f Binary files /dev/null and b/thirdparty/dist/python_ldap-3.4.5.tar.gz differ diff --git a/thirdparty/dist/python_ldap-3.4.5.tar.gz.ABOUT b/thirdparty/dist/python_ldap-3.4.5.tar.gz.ABOUT new file mode 100644 index 00000000..f538d5ff --- /dev/null +++ b/thirdparty/dist/python_ldap-3.4.5.tar.gz.ABOUT @@ -0,0 +1,76 @@ +about_resource: python_ldap-3.4.5.tar.gz +name: python-ldap +version: 3.4.5 +download_url: https://files.pythonhosted.org/packages/0c/88/8d2797decc42e1c1cdd926df4f005e938b0643d0d1219c08c2b5ee8ae0c0/python_ldap-3.4.5.tar.gz +description: "Python modules for implementing LDAP clients\n---------------------------------------\n\ + python-ldap: LDAP client API for Python\n---------------------------------------\n\nWhat is\ + \ python-ldap?\n====================\n\npython-ldap provides an object-oriented API to access\ + \ LDAP\ndirectory servers from Python programs. Mainly it wraps the\nOpenLDAP client libs\ + \ for that purpose.\n\nAdditionally the package contains modules for other LDAP-related\n\ + stuff (e.g. processing LDIF, LDAPURLs, LDAPv3 sub-schema, etc.).\n\nNot included: Direct BER\ + \ support\n\nSee INSTALL for version compatibility\n\nSee TODO for planned features. Contributors\ + \ welcome.\n\nFor module documentation, see:\n\n\thttps://www.python-ldap.org/\n\nQuick usage\ + \ example:\n====================\n\n.. code-block:: python\n\n import ldap\n l = ldap.initialize(\"\ + ldap://my_ldap_server.my_domain\")\n l.simple_bind_s(\"\",\"\")\n l.search_s(\"o=My\ + \ Organisation, c=AU\", ldap.SCOPE_SUBTREE, \"objectclass=*\")\n\nSee directory ``Demo/``\ + \ of source distribution package for more\nexample code.\n\nAuthor(s) contact and documentation:\n\ + ====================================\n\n https://www.python-ldap.org/\n\nIf you are looking\ + \ for help, please try the mailing list archives\nfirst, then send a question to the mailing\ + \ list.\nBe warned that questions will be ignored if they can be\ntrivially answered by referring\ + \ to the documentation.\n\nIf you are interested in helping, please contact the mailing list.\n\ + If you want new features or upgrades, please check the mailing list\narchives and then enquire\ + \ about any progress.\n\nAcknowledgements:\n=================\n\nThanks to Konstantin Chuguev\ + \ \nand Steffen Ries for\ + \ working\non support for OpenLDAP 2.0.x features.\n\nThanks to Michael Stroeder for the\nmodules ``ldif``, ``ldapurl``, ``ldap/schema/*.py``, ``ldap/*.py``\ + \ and ``ldap/controls/*.py``.\n\nThanks to Hans Aschauer \n\ + for the C wrapper schema and SASL support.\n\nThanks to Mauro Cicognini \ + \ for the\nWIN32/MSVC6 bits, and the pre-built WIN32 ``ldap.pyd``.\n\nThanks to Waldemar Osuch\ + \ for contributing\nthe new-style docs based on reStructuredText.\n\ + \nThanks to Torsten Kurbad for the\neasy_install support.\n\nThanks\ + \ to James Andrewartha for\nsignificant contribution to ``Doc/*.tex``.\n\ + \nThanks to Rich Megginson for extending\nsupport for LDAPv3 controls\ + \ and adding support for LDAPv3 extended\noperations.\n\nThanks to Peter Gietz, DAASI for\ + \ funding some control modules.\n\nThanks to Chris Mikkelson for various fixes and ldap.syncrepl.\n\ + \nThese very kind people have supplied patches or suggested changes:\n\n* Federico Di Gregorio\ + \ \n* John Benninghoff \n* Donn Cave \n* Jason Gunthorpe \n* gurney_j \n\ + * Eric S. Johansson \n* David Margrave \n\ + * Uche Ogbuji \n* Neale Pickett \n* Blake\ + \ Weston \n* Wido Depping \n\ + * Deepak Giridharagopal \n* Ingo Steuwer \n\ + * Andreas Hasenack \n* Matej Vela \n\nThese\ + \ people contributed to Python 3 porting (at https://github.com/pyldap/):\n\n* ​A. Karl Kornel\n\ + * Alex Willmer\n* Aymeric Augustin\n* Bradley Baetz\n* Christian Heimes\n* Dirk Mueller\n\ + * Jon Dufresne\n* Martin Basti\n* Miro Hrončok\n* Paul Aurich\n* Petr Viktorin\n* Pieterjan\ + \ De Potter\n* Raphaël Barrois\n* Robert Kuska\n* Stanislav Láznička\n* Tobias Bräutigam\n\ + * Tom van Dijk\n* Wentao Han\n* William Brown\n\nThanks to all the guys on the python-ldap\ + \ mailing list for\ntheir contributions and input into this package.\n\n Thanks! We may\ + \ have missed someone: please mail us if we have omitted\n your name.\n\nLicence\n=======\n\ + \nThe python-ldap project comes with a LICENCE file.\n\nWe are aware that its text is unclear,\ + \ but it cannot be changed:\nall authors of python-ldap would need to approve the licence\ + \ change,\nbut a complete list of all the authors is not available.\n(Note that the Git repository\ + \ of the project is incomplete.\nFurthermore, commits imported from CVS lack authorship information;\ + \ users\n\"stroeder\" or \"leonard\" are commiters (reviewers), but sometimes not\nauthors\ + \ of the committed code.)\n\nThe current maintainers assume that the license is the sentence\ + \ that refers\nto \"Python-style license\" and assume this means a highly permissive open\ + \ source\nlicense that only requires preservation of the text of the LICENCE file\n(including\ + \ the disclaimer paragraph).\n\n-------------------------------------------------------------------------------\n\ + \nAll contributions committed since July 1st, 2021, as well as some past\ncontributions, are\ + \ licensed under the MIT license.\nThe MIT licence and more details are listed in the file\ + \ LICENCE.MIT." +homepage_url: https://www.python-ldap.org/ +package_url: pkg:pypi/python-ldap@3.4.5 +license_expression: python-ldap AND python +copyright: Copyright python-ldap contributors +attribute: yes +track_changes: yes +checksum_md5: ed363c1fa9767f865dcb18c7bcc9f931 +checksum_sha1: 7a2dcfcf68019f15d7aa71f2a527494b8bf0fb29 +licenses: + - key: python + name: Python Software Foundation License v2 + file: python.LICENSE + - key: python-ldap + name: Python ldap License + file: python-ldap.LICENSE diff --git a/thirdparty/dist/pytz-2025.2-py2.py3-none-any.whl b/thirdparty/dist/pytz-2025.2-py2.py3-none-any.whl new file mode 100644 index 00000000..2363fabb Binary files /dev/null and b/thirdparty/dist/pytz-2025.2-py2.py3-none-any.whl differ diff --git a/thirdparty/dist/pytz-2025.2-py2.py3-none-any.whl.ABOUT b/thirdparty/dist/pytz-2025.2-py2.py3-none-any.whl.ABOUT new file mode 100644 index 00000000..d1492e18 --- /dev/null +++ b/thirdparty/dist/pytz-2025.2-py2.py3-none-any.whl.ABOUT @@ -0,0 +1,14 @@ +about_resource: pytz-2025.2-py2.py3-none-any.whl +name: pytz +version: '2025.2' +download_url: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl +package_url: pkg:pypi/pytz@2025.2 +license_expression: mit +copyright: Copyright pytz project contributors +attribute: yes +checksum_md5: 51ad498990b93bab1efdb6ecce983474 +checksum_sha1: fe7938b9d92a0da600ee2af52538405e769fbca4 +licenses: + - key: mit + name: MIT License + file: mit.LICENSE diff --git a/thirdparty/dist/pyyaml-6.0.2.tar.gz b/thirdparty/dist/pyyaml-6.0.2.tar.gz deleted file mode 100644 index 69a538f6..00000000 Binary files a/thirdparty/dist/pyyaml-6.0.2.tar.gz and /dev/null differ diff --git a/thirdparty/dist/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl b/thirdparty/dist/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl new file mode 100644 index 00000000..ff9fd486 Binary files /dev/null and b/thirdparty/dist/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl differ diff --git a/thirdparty/dist/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl b/thirdparty/dist/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl new file mode 100644 index 00000000..61918450 Binary files /dev/null and b/thirdparty/dist/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl differ diff --git a/thirdparty/dist/pyyaml-6.0.3.tar.gz b/thirdparty/dist/pyyaml-6.0.3.tar.gz new file mode 100644 index 00000000..bcac6570 Binary files /dev/null and b/thirdparty/dist/pyyaml-6.0.3.tar.gz differ diff --git a/thirdparty/dist/qrcode-8.0-py3-none-any.whl b/thirdparty/dist/qrcode-8.0-py3-none-any.whl deleted file mode 100644 index 6964bd40..00000000 Binary files a/thirdparty/dist/qrcode-8.0-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/qrcode-8.0-py3-none-any.whl.ABOUT b/thirdparty/dist/qrcode-8.0-py3-none-any.whl.ABOUT deleted file mode 100644 index 4c29605c..00000000 --- a/thirdparty/dist/qrcode-8.0-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,17 +0,0 @@ -about_resource: qrcode-8.0-py3-none-any.whl -name: qrcode -version: '8.0' -download_url: https://files.pythonhosted.org/packages/74/ab/df8d889fd01139db68ae9e5cb5c8f0ea016823559a6ecb427582d52b07dc/qrcode-8.0-py3-none-any.whl -package_url: pkg:pypi/qrcode@8.0 -license_expression: bsd-new AND proprietary-license -copyright: Copyright qrcode project contributors -attribute: yes -checksum_md5: 9357c249119c742ee5c226e6212c0315 -checksum_sha1: 1e2d4ce37fb3486113d304f186226fe490b92e1f -licenses: - - key: bsd-new - name: BSD-3-Clause - file: bsd-new.LICENSE - - key: proprietary-license - name: Proprietary License - file: proprietary-license.LICENSE diff --git a/thirdparty/dist/qrcode-8.2-py3-none-any.whl b/thirdparty/dist/qrcode-8.2-py3-none-any.whl new file mode 100644 index 00000000..eab6ee97 Binary files /dev/null and b/thirdparty/dist/qrcode-8.2-py3-none-any.whl differ diff --git a/thirdparty/dist/qrcode-8.2-py3-none-any.whl.ABOUT b/thirdparty/dist/qrcode-8.2-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..0ff982e6 --- /dev/null +++ b/thirdparty/dist/qrcode-8.2-py3-none-any.whl.ABOUT @@ -0,0 +1,17 @@ +about_resource: qrcode-8.2-py3-none-any.whl +name: qrcode +version: '8.2' +download_url: https://files.pythonhosted.org/packages/dd/b8/d2d6d731733f51684bbf76bf34dab3b70a9148e8f2cef2bb544fccec681a/qrcode-8.2-py3-none-any.whl +package_url: pkg:pypi/qrcode@8.2 +license_expression: bsd-new AND proprietary-license +copyright: Copyright qrcode project contributors +attribute: yes +checksum_md5: 569dcb4ddf30cf606654f79506d57415 +checksum_sha1: d4f78a51b490042cce4fe0d689a7c6854d1efd7b +licenses: + - key: bsd-new + name: BSD-3-Clause + file: bsd-new.LICENSE + - key: proprietary-license + name: Proprietary License + file: proprietary-license.LICENSE diff --git a/thirdparty/dist/redis-5.2.1-py3-none-any.whl b/thirdparty/dist/redis-5.2.1-py3-none-any.whl deleted file mode 100644 index 604c6822..00000000 Binary files a/thirdparty/dist/redis-5.2.1-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/redis-5.2.1-py3-none-any.whl.ABOUT b/thirdparty/dist/redis-5.2.1-py3-none-any.whl.ABOUT deleted file mode 100644 index 1b0976c0..00000000 --- a/thirdparty/dist/redis-5.2.1-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,14 +0,0 @@ -about_resource: redis-5.2.1-py3-none-any.whl -name: redis -version: 5.2.1 -download_url: https://files.pythonhosted.org/packages/3c/5f/fa26b9b2672cbe30e07d9a5bdf39cf16e3b80b42916757c5f92bca88e4ba/redis-5.2.1-py3-none-any.whl -package_url: pkg:pypi/redis@5.2.1 -license_expression: mit -copyright: Copyright redis project contributors -attribute: yes -checksum_md5: 5aec1e9dff0bebbaa6d8139dd8a142a1 -checksum_sha1: 659c02e8c12377f54fe79fb7bc5eb25448d0d7d2 -licenses: - - key: mit - name: MIT License - file: mit.LICENSE diff --git a/thirdparty/dist/redis-7.3.0-py3-none-any.whl b/thirdparty/dist/redis-7.3.0-py3-none-any.whl new file mode 100644 index 00000000..0defff1c Binary files /dev/null and b/thirdparty/dist/redis-7.3.0-py3-none-any.whl differ diff --git a/thirdparty/dist/redis-7.3.0-py3-none-any.whl.ABOUT b/thirdparty/dist/redis-7.3.0-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..dcce86d9 --- /dev/null +++ b/thirdparty/dist/redis-7.3.0-py3-none-any.whl.ABOUT @@ -0,0 +1,14 @@ +about_resource: redis-7.3.0-py3-none-any.whl +name: redis +version: 7.3.0 +download_url: https://files.pythonhosted.org/packages/f0/28/84e57fce7819e81ec5aa1bd31c42b89607241f4fb1a3ea5b0d2dbeaea26c/redis-7.3.0-py3-none-any.whl +package_url: pkg:pypi/redis@7.3.0 +license_expression: mit +copyright: Copyright redis project contributors +attribute: yes +checksum_md5: d325df6ff9304b16c23445ba77b774f4 +checksum_sha1: ac668056b0c7a5a570a2e976341fab7766792e98 +licenses: + - key: mit + name: MIT License + file: mit.LICENSE diff --git a/thirdparty/dist/referencing-0.36.2-py3-none-any.whl b/thirdparty/dist/referencing-0.36.2-py3-none-any.whl deleted file mode 100644 index e99c9a0d..00000000 Binary files a/thirdparty/dist/referencing-0.36.2-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/referencing-0.36.2-py3-none-any.whl.ABOUT b/thirdparty/dist/referencing-0.36.2-py3-none-any.whl.ABOUT deleted file mode 100644 index 239237d7..00000000 --- a/thirdparty/dist/referencing-0.36.2-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,14 +0,0 @@ -about_resource: referencing-0.36.2-py3-none-any.whl -name: referencing -version: 0.36.2 -download_url: https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl -package_url: pkg:pypi/referencing@0.36.2 -license_expression: mit -copyright: Copyright referencing project contributors -attribute: yes -checksum_md5: d531b05b57e25723830b8f1b7e7e3644 -checksum_sha1: c3df82240b04f1401dfdc687af2cf0092005454e -licenses: - - key: mit - name: MIT License - file: mit.LICENSE diff --git a/thirdparty/dist/referencing-0.37.0-py3-none-any.whl b/thirdparty/dist/referencing-0.37.0-py3-none-any.whl new file mode 100644 index 00000000..b2b482d6 Binary files /dev/null and b/thirdparty/dist/referencing-0.37.0-py3-none-any.whl differ diff --git a/thirdparty/dist/referencing-0.37.0-py3-none-any.whl.ABOUT b/thirdparty/dist/referencing-0.37.0-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..fbefef74 --- /dev/null +++ b/thirdparty/dist/referencing-0.37.0-py3-none-any.whl.ABOUT @@ -0,0 +1,14 @@ +about_resource: referencing-0.37.0-py3-none-any.whl +name: referencing +version: 0.37.0 +download_url: https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl +package_url: pkg:pypi/referencing@0.37.0 +license_expression: mit +copyright: Copyright referencing project contributors +attribute: yes +checksum_md5: 0be0d0a9f3691df1a295018982a60ce9 +checksum_sha1: d38a45a8772a997bf4e14a52bc1745e6a8458f68 +licenses: + - key: mit + name: MIT License + file: mit.LICENSE diff --git a/thirdparty/dist/requests-2.31.0-py3-none-any.whl b/thirdparty/dist/requests-2.31.0-py3-none-any.whl deleted file mode 100644 index bfd5d2ea..00000000 Binary files a/thirdparty/dist/requests-2.31.0-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/requests-2.31.0-py3-none-any.whl.ABOUT b/thirdparty/dist/requests-2.31.0-py3-none-any.whl.ABOUT deleted file mode 100644 index 4adbdb36..00000000 --- a/thirdparty/dist/requests-2.31.0-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,19 +0,0 @@ -about_resource: requests-2.31.0-py3-none-any.whl -name: requests -version: 2.31.0 -download_url: https://files.pythonhosted.org/packages/70/8e/0e2d847013cb52cd35b38c009bb167a1a26b2ce6cd6965bf26b47bc0bf44/requests-2.31.0-py3-none-any.whl -homepage_url: https://pypi.org/project/requests/2.31.0/ -package_url: pkg:pypi/requests@2.31.0 -license_expression: apache-2.0 -copyright: Copyright requests project contributors -attribute: yes -track_changes: yes -owner: Kenneth Reitz -owner_url: https://github.com/kennethreitz -contact: me@kennethreitz.com -checksum_md5: 0cb4b772a1a652cf3d170a6c42a69098 -checksum_sha1: 60b928b15e05d04a33b880a0232e44258c777740 -licenses: - - key: apache-2.0 - name: Apache License 2.0 - file: apache-2.0.LICENSE diff --git a/thirdparty/dist/requests-2.32.3-py3-none-any.whl b/thirdparty/dist/requests-2.32.3-py3-none-any.whl deleted file mode 100644 index 23662ce7..00000000 Binary files a/thirdparty/dist/requests-2.32.3-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/requests-2.33.0-py3-none-any.whl b/thirdparty/dist/requests-2.33.0-py3-none-any.whl new file mode 100644 index 00000000..072cf18d Binary files /dev/null and b/thirdparty/dist/requests-2.33.0-py3-none-any.whl differ diff --git a/thirdparty/dist/requests-2.33.0-py3-none-any.whl.ABOUT b/thirdparty/dist/requests-2.33.0-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..64cab5e1 --- /dev/null +++ b/thirdparty/dist/requests-2.33.0-py3-none-any.whl.ABOUT @@ -0,0 +1,15 @@ +about_resource: requests-2.33.0-py3-none-any.whl +name: requests +version: 2.33.0 +download_url: https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl +package_url: pkg:pypi/requests@2.33.0 +license_expression: apache-2.0 +copyright: Copyright requests project contributors +attribute: yes +track_changes: yes +checksum_md5: 22daaf63bcca957bce99d803119a355d +checksum_sha1: 3f6a1964b6d6535771e13f9f7789a2cb7f8f73dd +licenses: + - key: apache-2.0 + name: Apache License 2.0 + file: apache-2.0.LICENSE diff --git a/thirdparty/dist/requests_oauthlib-1.3.1-py2.py3-none-any.whl b/thirdparty/dist/requests_oauthlib-1.3.1-py2.py3-none-any.whl deleted file mode 100644 index 175d6d4e..00000000 Binary files a/thirdparty/dist/requests_oauthlib-1.3.1-py2.py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/requests_oauthlib-1.3.1-py2.py3-none-any.whl.ABOUT b/thirdparty/dist/requests_oauthlib-1.3.1-py2.py3-none-any.whl.ABOUT deleted file mode 100644 index f759eaf4..00000000 --- a/thirdparty/dist/requests_oauthlib-1.3.1-py2.py3-none-any.whl.ABOUT +++ /dev/null @@ -1,19 +0,0 @@ -about_resource: requests_oauthlib-1.3.1-py2.py3-none-any.whl -name: requests-oauthlib -version: 1.3.1 -download_url: https://files.pythonhosted.org/packages/6f/bb/5deac77a9af870143c684ab46a7934038a53eb4aa975bc0687ed6ca2c610/requests_oauthlib-1.3.1-py2.py3-none-any.whl -homepage_url: https://pypi.org/project/requests-oauthlib/1.3.1/ -package_url: pkg:pypi/requests-oauthlib@1.3.1 -license_expression: isc -copyright: Copyright (c) Kenneth Reitz -notice_file: requests_oauthlib-1.3.1-py2.py3-none-any.whl.NOTICE -attribute: yes -owner: Kenneth Reitz -owner_url: https://github.com/kennethreitz -contact: me@kennethreitz.com -checksum_md5: 24f97ba7181f2f105936db6932c1dbb5 -checksum_sha1: 453668972bbb7580e6938f6896efaa38645415e2 -licenses: - - key: isc - name: ISC License - file: isc.LICENSE diff --git a/thirdparty/dist/requests_oauthlib-1.3.1-py2.py3-none-any.whl.NOTICE b/thirdparty/dist/requests_oauthlib-1.3.1-py2.py3-none-any.whl.NOTICE deleted file mode 100644 index ad6708ea..00000000 --- a/thirdparty/dist/requests_oauthlib-1.3.1-py2.py3-none-any.whl.NOTICE +++ /dev/null @@ -1,11 +0,0 @@ -Permission to use, copy, modify, and/or distribute this software for any -purpose with or without fee is hereby granted, provided that the above -copyright notice and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR -ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF -OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. \ No newline at end of file diff --git a/thirdparty/dist/requests_oauthlib-2.0.0-py2.py3-none-any.whl.ABOUT b/thirdparty/dist/requests_oauthlib-2.0.0-py2.py3-none-any.whl.ABOUT new file mode 100644 index 00000000..d79a37b6 --- /dev/null +++ b/thirdparty/dist/requests_oauthlib-2.0.0-py2.py3-none-any.whl.ABOUT @@ -0,0 +1,17 @@ +about_resource: requests_oauthlib-2.0.0-py2.py3-none-any.whl +name: requests-oauthlib +version: 2.0.0 +download_url: https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl +package_url: pkg:pypi/requests-oauthlib@2.0.0 +license_expression: bsd-new AND isc +copyright: Copyright requests-oauthlib project contributors +attribute: yes +checksum_md5: 68df2f3e274ac34fb2c5f32b15374156 +checksum_sha1: 6ffca7abd905afcd371ddcb95ef58ecaa0ffeba6 +licenses: + - key: bsd-new + name: BSD-3-Clause + file: bsd-new.LICENSE + - key: isc + name: ISC License + file: isc.LICENSE diff --git a/thirdparty/dist/restructuredtext_lint-1.4.0.tar.gz b/thirdparty/dist/restructuredtext_lint-1.4.0.tar.gz deleted file mode 100644 index 68260e09..00000000 Binary files a/thirdparty/dist/restructuredtext_lint-1.4.0.tar.gz and /dev/null differ diff --git a/thirdparty/dist/restructuredtext_lint-1.4.0.tar.gz.ABOUT b/thirdparty/dist/restructuredtext_lint-1.4.0.tar.gz.ABOUT deleted file mode 100644 index 5b802f11..00000000 --- a/thirdparty/dist/restructuredtext_lint-1.4.0.tar.gz.ABOUT +++ /dev/null @@ -1,250 +0,0 @@ -about_resource: restructuredtext_lint-1.4.0.tar.gz -name: restructuredtext-lint -version: 1.4.0 -download_url: https://files.pythonhosted.org/packages/48/9c/6d8035cafa2d2d314f34e6cd9313a299de095b26e96f1c7312878f988eec/restructuredtext_lint-1.4.0.tar.gz -description: | - reStructuredText linter - restructuredtext-lint - ===================== - - .. image:: https://travis-ci.org/twolfson/restructuredtext-lint.png?branch=master - :target: https://travis-ci.org/twolfson/restructuredtext-lint - :alt: Build Status - - `reStructuredText`_ `linter`_ - - This was created out of frustration with `PyPI`_; it sucks finding out your `reST`_ is invalid **after** uploading it. It is being developed in junction with a `Sublime Text`_ linter. - - .. _`reStructuredText`: http://docutils.sourceforge.net/rst.html - .. _`linter`: http://en.wikipedia.org/wiki/Lint_%28software%29 - .. _`reST`: `reStructuredText`_ - .. _`PyPI`: http://pypi.python.org/ - .. _`Sublime Text`: http://sublimetext.com/ - - Getting Started - --------------- - Install the module with: ``pip install restructuredtext_lint`` - - .. code:: python - - import restructuredtext_lint - errors = restructuredtext_lint.lint(""" - Hello World - ======= - """) - - # `errors` will be list of system messages - # [>] - errors[0].message # Title underline too short. - - CLI Utility - ^^^^^^^^^^^ - For your convenience, we present a CLI utility ``rst-lint`` (also available as ``restructuredtext-lint``). - - .. code:: console - - $ rst-lint --help - usage: rst-lint [-h] [--version] [--format {text,json}] [--encoding ENCODING] - [--level {debug,info,warning,error,severe}] - [--rst-prolog RST_PROLOG] - path [path ...] - - Lint reStructuredText files. Returns 0 if all files pass linting, 1 for an - internal error, and 2 if linting failed. - - positional arguments: - path File/folder to lint - - optional arguments: - -h, --help show this help message and exit - --version show program's version number and exit - --format {text,json} Format of the output (default: "text") - --encoding ENCODING Encoding of the input file (e.g. "utf-8") - --level {debug,info,warning,error,severe} - Minimum error level to report (default: "warning") - --rst-prolog RST_PROLOG - reStructuredText content to prepend to all files - (useful for substitutions) - - $ rst-lint README.rst - WARNING README.rst:2 Title underline too short. - - Other tools - ^^^^^^^^^^^ - ``restructuredtext-lint`` is also integrated in other tools. A list can be found and updated in our wiki - - https://github.com/twolfson/restructuredtext-lint/wiki/Integration-in-other-tools - - PyPI issues - ^^^^^^^^^^^ - While a document may lint cleanly locally, there can be issues when submitted it to `PyPI`_. Here are some common problems: - - - Usage of non-builtin lexers (e.g. ``bibtex``) will pass locally but not be recognized/parsable on `PyPI`_ - - - This is due to `PyPI`_ not having a non-builtin lexer installed - - Please avoid non-builtin lexers to avoid complications - - For more information, see `#27`_ - - - Relative hyperlinks will not work (e.g. ``./UNLICENSE``) - - - According to Stack Overflow, hyperlinks must use a scheme (e.g. ``http``, ``https``) and that scheme must be whitelisted - - - http://stackoverflow.com/a/16594755 - - - Please use absolute hyperlinks (e.g. ``https://github.com/twolfson/restructuredtext-lint/blob/master/UNLICENSE``) - - .. _`#27`: https://github.com/twolfson/restructuredtext-lint/issues/27 - - Documentation - ------------- - ``restructuredtext-lint`` exposes a ``lint`` and ``lint_file`` function - - ``restructuredtext_lint.lint(content, filepath=None, rst_prolog=None)`` - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - Lint `reStructuredText`_ and return errors - - - content ``String`` - `reStructuredText`_ to be linted - - filepath ``String`` - Optional path to file, this will be returned as the source - - rst_prolog ``String`` - Optional content to prepend to content, line numbers will be offset to ignore this - - Returns: - - - errors ``List`` - List of errors - - - Each error is a class from `docutils`_ with the following attrs - - - line ``Integer|None`` - Line where the error occurred - - - On rare occasions, this will be ``None`` (e.g. anonymous link mismatch) - - - source ``String`` - ``filepath`` provided in parameters - - level ``Integer`` - Level of the warning - - - Levels represent 'info': 1, 'warning': 2, 'error': 3, 'severe': 4 - - - type ``String`` - Noun describing the error level - - - Levels can be 'INFO', 'WARNING', 'ERROR', or 'SEVERE' - - message ``String`` - Error message - - full_message ``String`` - Error message and source lines where the error occurred - - - It should be noted that ``level``, ``type``, ``message``, and ``full_message`` are custom attrs added onto the original ``system_message`` - - .. _`docutils`: http://docutils.sourceforge.net/ - - ``restructuredtext_lint.lint_file(filepath, encoding=None, *args, **kwargs)`` - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - Lint a `reStructuredText`_ file and return errors - - - filepath ``String`` - Path to file for linting - - encoding ``String`` - Encoding to read file in as - - - When ``None`` is provided, it will use OS default as provided by `locale.getpreferredencoding`_ - - The list of supported encodings can be found at http://docs.python.org/2/library/codecs.html#standard-encodings - - - ``*args`` - Additional arguments to be passed to ``lint`` - - ``**kwargs`` - Additional keyword arguments to be passed to ``lint`` - - .. _`locale.getpreferredencoding`: http://docs.python.org/2/library/locale.html#locale.getpreferredencoding - - Returns: Same structure as ``restructuredtext_lint.lint`` - - Extension - --------- - Under the hood, we leverage `docutils`_ for parsing reStructuredText documents. `docutils`_ supports adding new directives and roles via ``register_directive`` and ``register_role``. - - Sphinx - ^^^^^^ - Unfortunately due to customizations in `Sphinx's parser`_ we cannot include all of its directives/roles (see `#29`_). However, we can include some of them as one-offs. Here is an example of adding a directive from `Sphinx`_. - - .. _`Sphinx`: http://sphinx-doc.org/ - .. _`Sphinx's parser`: Sphinx_ - .. _`#29`: https://github.com/twolfson/restructuredtext-lint/issues/29#issuecomment-243456787 - - https://github.com/sphinx-doc/sphinx/blob/1.3/sphinx/directives/code.py - - **sphinx.rst** - - .. code:: rst - - Hello - ===== - World - - .. highlight:: python - - Hello World! - - **sphinx.py** - - .. code:: python - - # Load in our dependencies - from docutils.parsers.rst.directives import register_directive - from sphinx.directives.code import Highlight - import restructuredtext_lint - - # Load our new directive - register_directive('highlight', Highlight) - - # Lint our README - errors = restructuredtext_lint.lint_file('docs/sphinx/README.rst') - print errors[0].message # Error in "highlight" directive: no content permitted. - - Examples - -------- - Here is an example of all invalid properties - - .. code:: python - - rst = """ - Some content. - - Hello World - ======= - Some more content! - """ - errors = restructuredtext_lint.lint(rst, 'myfile.py') - errors[0].line # 5 - errors[0].source # myfile.py - errors[0].level # 2 - errors[0].type # WARNING - errors[0].message # Title underline too short. - errors[0].full_message # Title underline too short. - # - # Hello World - # ======= - - Contributing - ------------ - In lieu of a formal styleguide, take care to maintain the existing coding style. Add unit tests for any new or changed functionality. Test via ``nosetests``. - - Donating - -------- - Support this project and `others by twolfson`_ via `donations`_. - - http://twolfson.com/support-me - - .. _`others by twolfson`: http://twolfson.com/projects - .. _donations: http://twolfson.com/support-me - - Unlicense - --------- - As of Nov 22 2013, Todd Wolfson has released this repository and its contents to the public domain. - - It has been released under the `UNLICENSE`_. - - .. _UNLICENSE: https://github.com/twolfson/restructuredtext-lint/blob/master/UNLICENSE -homepage_url: https://github.com/twolfson/restructuredtext-lint -package_url: pkg:pypi/restructuredtext-lint@1.4.0 -license_expression: unlicense AND public-domain -copyright: Copyright restructuredtext-lint project contributors -checksum_md5: 05aae776c7fe02edb03f3b2601ac6b67 -checksum_sha1: 6de65a73428489ec1b8e654ad99335c9e540ba39 -licenses: - - key: public-domain - name: Public Domain - file: public-domain.LICENSE - - key: unlicense - name: Unlicense - file: unlicense.LICENSE diff --git a/thirdparty/dist/rpds_py-0.22.3-cp312-cp312-macosx_11_0_arm64.whl b/thirdparty/dist/rpds_py-0.22.3-cp312-cp312-macosx_11_0_arm64.whl deleted file mode 100644 index 3850431c..00000000 Binary files a/thirdparty/dist/rpds_py-0.22.3-cp312-cp312-macosx_11_0_arm64.whl and /dev/null differ diff --git a/thirdparty/dist/rpds_py-0.22.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl b/thirdparty/dist/rpds_py-0.22.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl deleted file mode 100644 index 83b9a710..00000000 Binary files a/thirdparty/dist/rpds_py-0.22.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl and /dev/null differ diff --git a/thirdparty/dist/rpds_py-0.22.3.tar.gz b/thirdparty/dist/rpds_py-0.22.3.tar.gz deleted file mode 100644 index c0b734d6..00000000 Binary files a/thirdparty/dist/rpds_py-0.22.3.tar.gz and /dev/null differ diff --git a/thirdparty/dist/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl b/thirdparty/dist/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl new file mode 100644 index 00000000..3f1bf5fc Binary files /dev/null and b/thirdparty/dist/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl differ diff --git a/thirdparty/dist/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl b/thirdparty/dist/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl new file mode 100644 index 00000000..ef96974b Binary files /dev/null and b/thirdparty/dist/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl differ diff --git a/thirdparty/dist/rpds_py-0.30.0.tar.gz b/thirdparty/dist/rpds_py-0.30.0.tar.gz new file mode 100644 index 00000000..f0c7c304 Binary files /dev/null and b/thirdparty/dist/rpds_py-0.30.0.tar.gz differ diff --git a/thirdparty/dist/rpds_py-0.22.3.tar.gz.ABOUT b/thirdparty/dist/rpds_py-0.30.0.tar.gz.ABOUT similarity index 90% rename from thirdparty/dist/rpds_py-0.22.3.tar.gz.ABOUT rename to thirdparty/dist/rpds_py-0.30.0.tar.gz.ABOUT index 9a319e51..049387cb 100644 --- a/thirdparty/dist/rpds_py-0.22.3.tar.gz.ABOUT +++ b/thirdparty/dist/rpds_py-0.30.0.tar.gz.ABOUT @@ -1,7 +1,7 @@ -about_resource: rpds_py-0.22.3.tar.gz +about_resource: rpds_py-0.30.0.tar.gz name: rpds-py -version: 0.22.3 -download_url: https://files.pythonhosted.org/packages/01/80/cce854d0921ff2f0a9fa831ba3ad3c65cee3a46711addf39a2af52df2cfd/rpds_py-0.22.3.tar.gz +version: 0.30.0 +download_url: https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz description: | Python bindings to Rust's persistent data structures (rpds) =========== @@ -73,12 +73,12 @@ description: | >>> L.rest == List([3, 5]) True homepage_url: https://github.com/crate-py/rpds -package_url: pkg:pypi/rpds-py@0.22.3 +package_url: pkg:pypi/rpds-py@0.30.0 license_expression: mit copyright: Copyright Julian Berman attribute: yes -checksum_md5: 461d6f4e753bfd1f26627f5520294b88 -checksum_sha1: dc24ac54fdb600bfe45246df1cd4338b8c22ca75 +checksum_md5: 9a598f792d702fd0547a36ccc999ef91 +checksum_sha1: 5fdab25b519123e7e53e5f2629a597b9921f129c licenses: - key: mit name: MIT License diff --git a/thirdparty/dist/rq-2.1.0-py3-none-any.whl b/thirdparty/dist/rq-2.1.0-py3-none-any.whl deleted file mode 100644 index dc87cf77..00000000 Binary files a/thirdparty/dist/rq-2.1.0-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/rq-2.1.0-py3-none-any.whl.ABOUT b/thirdparty/dist/rq-2.1.0-py3-none-any.whl.ABOUT deleted file mode 100644 index 43e3e553..00000000 --- a/thirdparty/dist/rq-2.1.0-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,14 +0,0 @@ -about_resource: rq-2.1.0-py3-none-any.whl -name: rq -version: 2.1.0 -download_url: https://files.pythonhosted.org/packages/3f/b3/e691454a551366c71248197f9050e4564f85d15c5d8a5c167ecac4411c40/rq-2.1.0-py3-none-any.whl -package_url: pkg:pypi/rq@2.1.0 -license_expression: bsd-new -copyright: Copyright rq project contributors -attribute: yes -checksum_md5: b6384a5eefdfee230c743c8c22856b07 -checksum_sha1: 8bf6aaa1fd99e2de256e9b7a07af5f6db5cee01d -licenses: - - key: bsd-new - name: BSD-3-Clause - file: bsd-new.LICENSE diff --git a/thirdparty/dist/rq-2.7.0-py3-none-any.whl b/thirdparty/dist/rq-2.7.0-py3-none-any.whl new file mode 100644 index 00000000..b7b87eeb Binary files /dev/null and b/thirdparty/dist/rq-2.7.0-py3-none-any.whl differ diff --git a/thirdparty/dist/rq-2.7.0-py3-none-any.whl.ABOUT b/thirdparty/dist/rq-2.7.0-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..ea69cf94 --- /dev/null +++ b/thirdparty/dist/rq-2.7.0-py3-none-any.whl.ABOUT @@ -0,0 +1,14 @@ +about_resource: rq-2.7.0-py3-none-any.whl +name: rq +version: 2.7.0 +download_url: https://files.pythonhosted.org/packages/0d/1a/3b64696bc0c33aa1d86d3e6add03c4e0afe51110264fd41208bd95c2665c/rq-2.7.0-py3-none-any.whl +package_url: pkg:pypi/rq@2.7.0 +license_expression: bsd-new +copyright: Copyright rq project contributors +attribute: yes +checksum_md5: 5a6184ae6d00d0267d1c8d4939b0a087 +checksum_sha1: bdc76c843a02b0104474fd65171c829a9cc03692 +licenses: + - key: bsd-new + name: BSD-3-Clause + file: bsd-new.LICENSE diff --git a/thirdparty/dist/rq_scheduler-0.14.0-py2.py3-none-any.whl b/thirdparty/dist/rq_scheduler-0.14.0-py2.py3-none-any.whl deleted file mode 100644 index 2713a86d..00000000 Binary files a/thirdparty/dist/rq_scheduler-0.14.0-py2.py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/rq_scheduler-0.14.0-py2.py3-none-any.whl.ABOUT b/thirdparty/dist/rq_scheduler-0.14.0-py2.py3-none-any.whl.ABOUT deleted file mode 100644 index b6ea83ca..00000000 --- a/thirdparty/dist/rq_scheduler-0.14.0-py2.py3-none-any.whl.ABOUT +++ /dev/null @@ -1,14 +0,0 @@ -about_resource: rq_scheduler-0.14.0-py2.py3-none-any.whl -name: rq-scheduler -version: 0.14.0 -download_url: https://files.pythonhosted.org/packages/bb/d0/28cedca9f3b321f30e69d644c2dcd7097ec21570ec9606fde56750621300/rq_scheduler-0.14.0-py2.py3-none-any.whl -package_url: pkg:pypi/rq-scheduler@0.14.0 -license_expression: mit -copyright: Copyright rq-scheduler project contributors -attribute: yes -checksum_md5: 3375aafb840fa099da62b14f3067e91c -checksum_sha1: 694d2ee9e73111a9f311094a9941ae844441e528 -licenses: - - key: mit - name: MIT License - file: mit.LICENSE diff --git a/thirdparty/dist/ruff-0.9.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl b/thirdparty/dist/ruff-0.15.0-py3-none-macosx_11_0_arm64.whl similarity index 51% rename from thirdparty/dist/ruff-0.9.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl rename to thirdparty/dist/ruff-0.15.0-py3-none-macosx_11_0_arm64.whl index 769e3794..13e5d58f 100644 Binary files a/thirdparty/dist/ruff-0.9.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl and b/thirdparty/dist/ruff-0.15.0-py3-none-macosx_11_0_arm64.whl differ diff --git a/thirdparty/dist/ruff-0.9.6-py3-none-macosx_11_0_arm64.whl b/thirdparty/dist/ruff-0.15.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl similarity index 57% rename from thirdparty/dist/ruff-0.9.6-py3-none-macosx_11_0_arm64.whl rename to thirdparty/dist/ruff-0.15.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl index 7cf3ba09..e51a9cd0 100644 Binary files a/thirdparty/dist/ruff-0.9.6-py3-none-macosx_11_0_arm64.whl and b/thirdparty/dist/ruff-0.15.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl differ diff --git a/thirdparty/dist/ruff-0.15.0.tar.gz b/thirdparty/dist/ruff-0.15.0.tar.gz new file mode 100644 index 00000000..88352655 Binary files /dev/null and b/thirdparty/dist/ruff-0.15.0.tar.gz differ diff --git a/thirdparty/dist/ruff-0.15.0.tar.gz.ABOUT b/thirdparty/dist/ruff-0.15.0.tar.gz.ABOUT new file mode 100644 index 00000000..046c8ec9 --- /dev/null +++ b/thirdparty/dist/ruff-0.15.0.tar.gz.ABOUT @@ -0,0 +1,569 @@ +about_resource: ruff-0.15.0.tar.gz +name: ruff +version: 0.15.0 +download_url: https://files.pythonhosted.org/packages/c8/39/5cee96809fbca590abea6b46c6d1c586b49663d1d2830a751cc8fc42c666/ruff-0.15.0.tar.gz +description: | + + + # Ruff + + [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) + [![image](https://img.shields.io/pypi/v/ruff.svg)](https://pypi.python.org/pypi/ruff) + [![image](https://img.shields.io/pypi/l/ruff.svg)](https://github.com/astral-sh/ruff/blob/main/LICENSE) + [![image](https://img.shields.io/pypi/pyversions/ruff.svg)](https://pypi.python.org/pypi/ruff) + [![Actions status](https://github.com/astral-sh/ruff/workflows/CI/badge.svg)](https://github.com/astral-sh/ruff/actions) + [![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?logo=discord&logoColor=white)](https://discord.com/invite/astral-sh) + + [**Docs**](https://docs.astral.sh/ruff/) | [**Playground**](https://play.ruff.rs/) + + An extremely fast Python linter and code formatter, written in Rust. + +

        + Shows a bar chart with benchmark results. +

        + +

        + Linting the CPython codebase from scratch. +

        + + - ⚡️ 10-100x faster than existing linters (like Flake8) and formatters (like Black) + - 🐍 Installable via `pip` + - 🛠️ `pyproject.toml` support + - 🤝 Python 3.14 compatibility + - ⚖️ Drop-in parity with [Flake8](https://docs.astral.sh/ruff/faq/#how-does-ruffs-linter-compare-to-flake8), isort, and [Black](https://docs.astral.sh/ruff/faq/#how-does-ruffs-formatter-compare-to-black) + - 📦 Built-in caching, to avoid re-analyzing unchanged files + - 🔧 Fix support, for automatic error correction (e.g., automatically remove unused imports) + - 📏 Over [800 built-in rules](https://docs.astral.sh/ruff/rules/), with native re-implementations + of popular Flake8 plugins, like flake8-bugbear + - ⌨️ First-party [editor integrations](https://docs.astral.sh/ruff/editors) for [VS Code](https://github.com/astral-sh/ruff-vscode) and [more](https://docs.astral.sh/ruff/editors/setup) + - 🌎 Monorepo-friendly, with [hierarchical and cascading configuration](https://docs.astral.sh/ruff/configuration/#config-file-discovery) + + Ruff aims to be orders of magnitude faster than alternative tools while integrating more + functionality behind a single, common interface. + + Ruff can be used to replace [Flake8](https://pypi.org/project/flake8/) (plus dozens of plugins), + [Black](https://github.com/psf/black), [isort](https://pypi.org/project/isort/), + [pydocstyle](https://pypi.org/project/pydocstyle/), [pyupgrade](https://pypi.org/project/pyupgrade/), + [autoflake](https://pypi.org/project/autoflake/), and more, all while executing tens or hundreds of + times faster than any individual tool. + + Ruff is extremely actively developed and used in major open-source projects like: + + - [Apache Airflow](https://github.com/apache/airflow) + - [Apache Superset](https://github.com/apache/superset) + - [FastAPI](https://github.com/tiangolo/fastapi) + - [Hugging Face](https://github.com/huggingface/transformers) + - [Pandas](https://github.com/pandas-dev/pandas) + - [SciPy](https://github.com/scipy/scipy) + + ...and [many more](#whos-using-ruff). + + Ruff is backed by [Astral](https://astral.sh), the creators of + [uv](https://github.com/astral-sh/uv) and [ty](https://github.com/astral-sh/ty). + + Read the [launch + post](https://astral.sh/blog/announcing-astral-the-company-behind-ruff), or the + original [project + announcement](https://notes.crmarsh.com/python-tooling-could-be-much-much-faster). + + ## Testimonials + + [**Sebastián Ramírez**](https://twitter.com/tiangolo/status/1591912354882764802), creator + of [FastAPI](https://github.com/tiangolo/fastapi): + + > Ruff is so fast that sometimes I add an intentional bug in the code just to confirm it's actually + > running and checking the code. + + [**Nick Schrock**](https://twitter.com/schrockn/status/1612615862904827904), founder of [Elementl](https://www.elementl.com/), + co-creator of [GraphQL](https://graphql.org/): + + > Why is Ruff a gamechanger? Primarily because it is nearly 1000x faster. Literally. Not a typo. On + > our largest module (dagster itself, 250k LOC) pylint takes about 2.5 minutes, parallelized across 4 + > cores on my M1. Running ruff against our _entire_ codebase takes .4 seconds. + + [**Bryan Van de Ven**](https://github.com/bokeh/bokeh/pull/12605), co-creator + of [Bokeh](https://github.com/bokeh/bokeh/), original author + of [Conda](https://docs.conda.io/en/latest/): + + > Ruff is ~150-200x faster than flake8 on my machine, scanning the whole repo takes ~0.2s instead of + > ~20s. This is an enormous quality of life improvement for local dev. It's fast enough that I added + > it as an actual commit hook, which is terrific. + + [**Timothy Crosley**](https://twitter.com/timothycrosley/status/1606420868514877440), + creator of [isort](https://github.com/PyCQA/isort): + + > Just switched my first project to Ruff. Only one downside so far: it's so fast I couldn't believe + > it was working till I intentionally introduced some errors. + + [**Tim Abbott**](https://github.com/zulip/zulip/pull/23431#issuecomment-1302557034), lead developer of [Zulip](https://github.com/zulip/zulip) (also [here](https://github.com/astral-sh/ruff/issues/465#issuecomment-1317400028)): + + > This is just ridiculously fast... `ruff` is amazing. + + + + ## Table of Contents + + For more, see the [documentation](https://docs.astral.sh/ruff/). + + 1. [Getting Started](#getting-started) + 1. [Configuration](#configuration) + 1. [Rules](#rules) + 1. [Contributing](#contributing) + 1. [Support](#support) + 1. [Acknowledgements](#acknowledgements) + 1. [Who's Using Ruff?](#whos-using-ruff) + 1. [License](#license) + + ## Getting Started + + For more, see the [documentation](https://docs.astral.sh/ruff/). + + ### Installation + + Ruff is available as [`ruff`](https://pypi.org/project/ruff/) on PyPI. + + Invoke Ruff directly with [`uvx`](https://docs.astral.sh/uv/): + + ```shell + uvx ruff check # Lint all files in the current directory. + uvx ruff format # Format all files in the current directory. + ``` + + Or install Ruff with `uv` (recommended), `pip`, or `pipx`: + + ```shell + # With uv. + uv tool install ruff@latest # Install Ruff globally. + uv add --dev ruff # Or add Ruff to your project. + + # With pip. + pip install ruff + + # With pipx. + pipx install ruff + ``` + + Starting with version `0.5.0`, Ruff can be installed with our standalone installers: + + ```shell + # On macOS and Linux. + curl -LsSf https://astral.sh/ruff/install.sh | sh + + # On Windows. + powershell -c "irm https://astral.sh/ruff/install.ps1 | iex" + + # For a specific version. + curl -LsSf https://astral.sh/ruff/0.15.0/install.sh | sh + powershell -c "irm https://astral.sh/ruff/0.15.0/install.ps1 | iex" + ``` + + You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff), + and with [a variety of other package managers](https://docs.astral.sh/ruff/installation/). + + ### Usage + + To run Ruff as a linter, try any of the following: + + ```shell + ruff check # Lint all files in the current directory (and any subdirectories). + ruff check path/to/code/ # Lint all files in `/path/to/code` (and any subdirectories). + ruff check path/to/code/*.py # Lint all `.py` files in `/path/to/code`. + ruff check path/to/code/to/file.py # Lint `file.py`. + ruff check @arguments.txt # Lint using an input file, treating its contents as newline-delimited command-line arguments. + ``` + + Or, to run Ruff as a formatter: + + ```shell + ruff format # Format all files in the current directory (and any subdirectories). + ruff format path/to/code/ # Format all files in `/path/to/code` (and any subdirectories). + ruff format path/to/code/*.py # Format all `.py` files in `/path/to/code`. + ruff format path/to/code/to/file.py # Format `file.py`. + ruff format @arguments.txt # Format using an input file, treating its contents as newline-delimited command-line arguments. + ``` + + Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff-pre-commit`](https://github.com/astral-sh/ruff-pre-commit): + + ```yaml + - repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.15.0 + hooks: + # Run the linter. + - id: ruff-check + args: [ --fix ] + # Run the formatter. + - id: ruff-format + ``` + + Ruff can also be used as a [VS Code extension](https://github.com/astral-sh/ruff-vscode) or with [various other editors](https://docs.astral.sh/ruff/editors/setup). + + Ruff can also be used as a [GitHub Action](https://github.com/features/actions) via + [`ruff-action`](https://github.com/astral-sh/ruff-action): + + ```yaml + name: Ruff + on: [ push, pull_request ] + jobs: + ruff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/ruff-action@v3 + ``` + + ### Configuration + + Ruff can be configured through a `pyproject.toml`, `ruff.toml`, or `.ruff.toml` file (see: + [_Configuration_](https://docs.astral.sh/ruff/configuration/), or [_Settings_](https://docs.astral.sh/ruff/settings/) + for a complete list of all configuration options). + + If left unspecified, Ruff's default configuration is equivalent to the following `ruff.toml` file: + + ```toml + # Exclude a variety of commonly ignored directories. + exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "site-packages", + "venv", + ] + + # Same as Black. + line-length = 88 + indent-width = 4 + + # Assume Python 3.9 + target-version = "py39" + + [lint] + # Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. + select = ["E4", "E7", "E9", "F"] + ignore = [] + + # Allow fix for all enabled rules (when `--fix`) is provided. + fixable = ["ALL"] + unfixable = [] + + # Allow unused variables when underscore-prefixed. + dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + + [format] + # Like Black, use double quotes for strings. + quote-style = "double" + + # Like Black, indent with spaces, rather than tabs. + indent-style = "space" + + # Like Black, respect magic trailing commas. + skip-magic-trailing-comma = false + + # Like Black, automatically detect the appropriate line ending. + line-ending = "auto" + ``` + + Note that, in a `pyproject.toml`, each section header should be prefixed with `tool.ruff`. For + example, `[lint]` should be replaced with `[tool.ruff.lint]`. + + Some configuration options can be provided via dedicated command-line arguments, such as those + related to rule enablement and disablement, file discovery, and logging level: + + ```shell + ruff check --select F401 --select F403 --quiet + ``` + + The remaining configuration options can be provided through a catch-all `--config` argument: + + ```shell + ruff check --config "lint.per-file-ignores = {'some_file.py' = ['F841']}" + ``` + + To opt in to the latest lint rules, formatter style changes, interface updates, and more, enable + [preview mode](https://docs.astral.sh/ruff/rules/) by setting `preview = true` in your configuration + file or passing `--preview` on the command line. Preview mode enables a collection of unstable + features that may change prior to stabilization. + + See `ruff help` for more on Ruff's top-level commands, or `ruff help check` and `ruff help format` + for more on the linting and formatting commands, respectively. + + ## Rules + + + + **Ruff supports over 800 lint rules**, many of which are inspired by popular tools like Flake8, + isort, pyupgrade, and others. Regardless of the rule's origin, Ruff re-implements every rule in + Rust as a first-party feature. + + By default, Ruff enables Flake8's `F` rules, along with a subset of the `E` rules, omitting any + stylistic rules that overlap with the use of a formatter, like `ruff format` or + [Black](https://github.com/psf/black). + + If you're just getting started with Ruff, **the default rule set is a great place to start**: it + catches a wide variety of common errors (like unused imports) with zero configuration. + + + + Beyond the defaults, Ruff re-implements some of the most popular Flake8 plugins and related code + quality tools, including: + + - [autoflake](https://pypi.org/project/autoflake/) + - [eradicate](https://pypi.org/project/eradicate/) + - [flake8-2020](https://pypi.org/project/flake8-2020/) + - [flake8-annotations](https://pypi.org/project/flake8-annotations/) + - [flake8-async](https://pypi.org/project/flake8-async) + - [flake8-bandit](https://pypi.org/project/flake8-bandit/) ([#1646](https://github.com/astral-sh/ruff/issues/1646)) + - [flake8-blind-except](https://pypi.org/project/flake8-blind-except/) + - [flake8-boolean-trap](https://pypi.org/project/flake8-boolean-trap/) + - [flake8-bugbear](https://pypi.org/project/flake8-bugbear/) + - [flake8-builtins](https://pypi.org/project/flake8-builtins/) + - [flake8-commas](https://pypi.org/project/flake8-commas/) + - [flake8-comprehensions](https://pypi.org/project/flake8-comprehensions/) + - [flake8-copyright](https://pypi.org/project/flake8-copyright/) + - [flake8-datetimez](https://pypi.org/project/flake8-datetimez/) + - [flake8-debugger](https://pypi.org/project/flake8-debugger/) + - [flake8-django](https://pypi.org/project/flake8-django/) + - [flake8-docstrings](https://pypi.org/project/flake8-docstrings/) + - [flake8-eradicate](https://pypi.org/project/flake8-eradicate/) + - [flake8-errmsg](https://pypi.org/project/flake8-errmsg/) + - [flake8-executable](https://pypi.org/project/flake8-executable/) + - [flake8-future-annotations](https://pypi.org/project/flake8-future-annotations/) + - [flake8-gettext](https://pypi.org/project/flake8-gettext/) + - [flake8-implicit-str-concat](https://pypi.org/project/flake8-implicit-str-concat/) + - [flake8-import-conventions](https://github.com/joaopalmeiro/flake8-import-conventions) + - [flake8-logging](https://pypi.org/project/flake8-logging/) + - [flake8-logging-format](https://pypi.org/project/flake8-logging-format/) + - [flake8-no-pep420](https://pypi.org/project/flake8-no-pep420) + - [flake8-pie](https://pypi.org/project/flake8-pie/) + - [flake8-print](https://pypi.org/project/flake8-print/) + - [flake8-pyi](https://pypi.org/project/flake8-pyi/) + - [flake8-pytest-style](https://pypi.org/project/flake8-pytest-style/) + - [flake8-quotes](https://pypi.org/project/flake8-quotes/) + - [flake8-raise](https://pypi.org/project/flake8-raise/) + - [flake8-return](https://pypi.org/project/flake8-return/) + - [flake8-self](https://pypi.org/project/flake8-self/) + - [flake8-simplify](https://pypi.org/project/flake8-simplify/) + - [flake8-slots](https://pypi.org/project/flake8-slots/) + - [flake8-super](https://pypi.org/project/flake8-super/) + - [flake8-tidy-imports](https://pypi.org/project/flake8-tidy-imports/) + - [flake8-todos](https://pypi.org/project/flake8-todos/) + - [flake8-type-checking](https://pypi.org/project/flake8-type-checking/) + - [flake8-use-pathlib](https://pypi.org/project/flake8-use-pathlib/) + - [flynt](https://pypi.org/project/flynt/) ([#2102](https://github.com/astral-sh/ruff/issues/2102)) + - [isort](https://pypi.org/project/isort/) + - [mccabe](https://pypi.org/project/mccabe/) + - [pandas-vet](https://pypi.org/project/pandas-vet/) + - [pep8-naming](https://pypi.org/project/pep8-naming/) + - [pydocstyle](https://pypi.org/project/pydocstyle/) + - [pygrep-hooks](https://github.com/pre-commit/pygrep-hooks) + - [pylint-airflow](https://pypi.org/project/pylint-airflow/) + - [pyupgrade](https://pypi.org/project/pyupgrade/) + - [tryceratops](https://pypi.org/project/tryceratops/) + - [yesqa](https://pypi.org/project/yesqa/) + + For a complete enumeration of the supported rules, see [_Rules_](https://docs.astral.sh/ruff/rules/). + + ## Contributing + + Contributions are welcome and highly appreciated. To get started, check out the + [**contributing guidelines**](https://docs.astral.sh/ruff/contributing/). + + You can also join us on [**Discord**](https://discord.com/invite/astral-sh). + + ## Support + + Having trouble? Check out the existing issues on [**GitHub**](https://github.com/astral-sh/ruff/issues), + or feel free to [**open a new one**](https://github.com/astral-sh/ruff/issues/new). + + You can also ask for help on [**Discord**](https://discord.com/invite/astral-sh). + + ## Acknowledgements + + Ruff's linter draws on both the APIs and implementation details of many other + tools in the Python ecosystem, especially [Flake8](https://github.com/PyCQA/flake8), [Pyflakes](https://github.com/PyCQA/pyflakes), + [pycodestyle](https://github.com/PyCQA/pycodestyle), [pydocstyle](https://github.com/PyCQA/pydocstyle), + [pyupgrade](https://github.com/asottile/pyupgrade), and [isort](https://github.com/PyCQA/isort). + + In some cases, Ruff includes a "direct" Rust port of the corresponding tool. + We're grateful to the maintainers of these tools for their work, and for all + the value they've provided to the Python community. + + Ruff's formatter is built on a fork of Rome's [`rome_formatter`](https://github.com/rome/tools/tree/main/crates/rome_formatter), + and again draws on both API and implementation details from [Rome](https://github.com/rome/tools), + [Prettier](https://github.com/prettier/prettier), and [Black](https://github.com/psf/black). + + Ruff's import resolver is based on the import resolution algorithm from [Pyright](https://github.com/microsoft/pyright). + + Ruff is also influenced by a number of tools outside the Python ecosystem, like + [Clippy](https://github.com/rust-lang/rust-clippy) and [ESLint](https://github.com/eslint/eslint). + + Ruff is the beneficiary of a large number of [contributors](https://github.com/astral-sh/ruff/graphs/contributors). + + Ruff is released under the MIT license. + + ## Who's Using Ruff? + + Ruff is used by a number of major open-source projects and companies, including: + + - [Albumentations](https://github.com/albumentations-team/AlbumentationsX) + - Amazon ([AWS SAM](https://github.com/aws/serverless-application-model)) + - [Anki](https://apps.ankiweb.net/) + - Anthropic ([Python SDK](https://github.com/anthropics/anthropic-sdk-python)) + - [Apache Airflow](https://github.com/apache/airflow) + - AstraZeneca ([Magnus](https://github.com/AstraZeneca/magnus-core)) + - [Babel](https://github.com/python-babel/babel) + - Benchling ([Refac](https://github.com/benchling/refac)) + - [Bokeh](https://github.com/bokeh/bokeh) + - Capital One ([datacompy](https://github.com/capitalone/datacompy)) + - CrowdCent ([NumerBlox](https://github.com/crowdcent/numerblox)) + - [Cryptography (PyCA)](https://github.com/pyca/cryptography) + - CERN ([Indico](https://getindico.io/)) + - [DVC](https://github.com/iterative/dvc) + - [Dagger](https://github.com/dagger/dagger) + - [Dagster](https://github.com/dagster-io/dagster) + - Databricks ([MLflow](https://github.com/mlflow/mlflow)) + - [Dify](https://github.com/langgenius/dify) + - [FastAPI](https://github.com/tiangolo/fastapi) + - [Godot](https://github.com/godotengine/godot) + - [Gradio](https://github.com/gradio-app/gradio) + - [Great Expectations](https://github.com/great-expectations/great_expectations) + - [HTTPX](https://github.com/encode/httpx) + - [Hatch](https://github.com/pypa/hatch) + - [Home Assistant](https://github.com/home-assistant/core) + - Hugging Face ([Transformers](https://github.com/huggingface/transformers), + [Datasets](https://github.com/huggingface/datasets), + [Diffusers](https://github.com/huggingface/diffusers)) + - IBM ([Qiskit](https://github.com/Qiskit/qiskit)) + - ING Bank ([popmon](https://github.com/ing-bank/popmon), [probatus](https://github.com/ing-bank/probatus)) + - [Ibis](https://github.com/ibis-project/ibis) + - [ivy](https://github.com/unifyai/ivy) + - [JAX](https://github.com/jax-ml/jax) + - [Jupyter](https://github.com/jupyter-server/jupyter_server) + - [Kraken Tech](https://kraken.tech/) + - [LangChain](https://github.com/hwchase17/langchain) + - [Litestar](https://litestar.dev/) + - [LlamaIndex](https://github.com/jerryjliu/llama_index) + - Matrix ([Synapse](https://github.com/matrix-org/synapse)) + - [MegaLinter](https://github.com/oxsecurity/megalinter) + - Meltano ([Meltano CLI](https://github.com/meltano/meltano), [Singer SDK](https://github.com/meltano/sdk)) + - Microsoft ([Semantic Kernel](https://github.com/microsoft/semantic-kernel), + [ONNX Runtime](https://github.com/microsoft/onnxruntime), + [LightGBM](https://github.com/microsoft/LightGBM)) + - Modern Treasury ([Python SDK](https://github.com/Modern-Treasury/modern-treasury-python)) + - Mozilla ([Firefox](https://github.com/mozilla/gecko-dev)) + - [Mypy](https://github.com/python/mypy) + - [Nautobot](https://github.com/nautobot/nautobot) + - Netflix ([Dispatch](https://github.com/Netflix/dispatch)) + - [Neon](https://github.com/neondatabase/neon) + - [Nokia](https://nokia.com/) + - [NoneBot](https://github.com/nonebot/nonebot2) + - [NumPyro](https://github.com/pyro-ppl/numpyro) + - [ONNX](https://github.com/onnx/onnx) + - [OpenBB](https://github.com/OpenBB-finance/OpenBBTerminal) + - [Open Wine Components](https://github.com/Open-Wine-Components/umu-launcher) + - [PDM](https://github.com/pdm-project/pdm) + - [PaddlePaddle](https://github.com/PaddlePaddle/Paddle) + - [Pandas](https://github.com/pandas-dev/pandas) + - [Pillow](https://github.com/python-pillow/Pillow) + - [Poetry](https://github.com/python-poetry/poetry) + - [Polars](https://github.com/pola-rs/polars) + - [PostHog](https://github.com/PostHog/posthog) + - Prefect ([Python SDK](https://github.com/PrefectHQ/prefect), [Marvin](https://github.com/PrefectHQ/marvin)) + - [PyInstaller](https://github.com/pyinstaller/pyinstaller) + - [PyMC](https://github.com/pymc-devs/pymc/) + - [PyMC-Marketing](https://github.com/pymc-labs/pymc-marketing) + - [pytest](https://github.com/pytest-dev/pytest) + - [PyTorch](https://github.com/pytorch/pytorch) + - [Pydantic](https://github.com/pydantic/pydantic) + - [Pylint](https://github.com/PyCQA/pylint) + - [PyScripter](https://github.com/pyscripter/pyscripter) + - [PyVista](https://github.com/pyvista/pyvista) + - [Reflex](https://github.com/reflex-dev/reflex) + - [River](https://github.com/online-ml/river) + - [Rippling](https://rippling.com) + - [Robyn](https://github.com/sansyrox/robyn) + - [Saleor](https://github.com/saleor/saleor) + - Scale AI ([Launch SDK](https://github.com/scaleapi/launch-python-client)) + - [SciPy](https://github.com/scipy/scipy) + - Snowflake ([SnowCLI](https://github.com/Snowflake-Labs/snowcli)) + - [Sphinx](https://github.com/sphinx-doc/sphinx) + - [Stable Baselines3](https://github.com/DLR-RM/stable-baselines3) + - [Starlette](https://github.com/encode/starlette) + - [Streamlit](https://github.com/streamlit/streamlit) + - [The Algorithms](https://github.com/TheAlgorithms/Python) + - [Vega-Altair](https://github.com/altair-viz/altair) + - [Weblate](https://weblate.org/) + - WordPress ([Openverse](https://github.com/WordPress/openverse)) + - [ZenML](https://github.com/zenml-io/zenml) + - [Zulip](https://github.com/zulip/zulip) + - [build (PyPA)](https://github.com/pypa/build) + - [cibuildwheel (PyPA)](https://github.com/pypa/cibuildwheel) + - [delta-rs](https://github.com/delta-io/delta-rs) + - [featuretools](https://github.com/alteryx/featuretools) + - [meson-python](https://github.com/mesonbuild/meson-python) + - [nox](https://github.com/wntrblm/nox) + - [pip](https://github.com/pypa/pip) + + ### Show Your Support + + If you're using Ruff, consider adding the Ruff badge to your project's `README.md`: + + ```md + [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) + ``` + + ...or `README.rst`: + + ```rst + .. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json + :target: https://github.com/astral-sh/ruff + :alt: Ruff + ``` + + ...or, as HTML: + + ```html + Ruff + ``` + + ## License + + This repository is licensed under the [MIT License](https://github.com/astral-sh/ruff/blob/main/LICENSE) + +
        + + Made by Astral + +
        +homepage_url: https://docs.astral.sh/ruff +package_url: pkg:pypi/ruff@0.15.0 +license_expression: mit +copyright: Copyright Anthony Sottile, Florent Xicluna +attribute: yes +checksum_md5: bbef1102be85f3491f0e081879113991 +checksum_sha1: 4729a8b0983fa49d8e0afd50e0903c8a73f79c5d +licenses: + - key: mit + name: MIT License + file: mit.LICENSE diff --git a/thirdparty/dist/ruff-0.9.6.tar.gz b/thirdparty/dist/ruff-0.9.6.tar.gz deleted file mode 100644 index a4e6a0f2..00000000 Binary files a/thirdparty/dist/ruff-0.9.6.tar.gz and /dev/null differ diff --git a/thirdparty/dist/saneyaml-0.6.0-py3-none-any.whl b/thirdparty/dist/saneyaml-0.6.0-py3-none-any.whl deleted file mode 100644 index 5552140b..00000000 Binary files a/thirdparty/dist/saneyaml-0.6.0-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/saneyaml-0.6.0-py3-none-any.whl.ABOUT b/thirdparty/dist/saneyaml-0.6.0-py3-none-any.whl.ABOUT deleted file mode 100644 index 935bf47a..00000000 --- a/thirdparty/dist/saneyaml-0.6.0-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,15 +0,0 @@ -about_resource: saneyaml-0.6.0-py3-none-any.whl -name: saneyaml -version: 0.6.0 -download_url: https://files.pythonhosted.org/packages/4e/08/b5216a1f1ecad5510975cbd5619c04cc615be25c4062214dca3a4a8ce957/saneyaml-0.6.0-py3-none-any.whl -package_url: pkg:pypi/saneyaml@0.6.0 -license_expression: apache-2.0 -copyright: Copyright saneyaml project contributors -attribute: yes -track_changes: yes -checksum_md5: affa2fb6c0ceb87044c2fb2972dacb68 -checksum_sha1: cfffdddce895e65cb2706e4a57e2fd92f40503f2 -licenses: - - key: apache-2.0 - name: Apache License 2.0 - file: apache-2.0.LICENSE diff --git a/thirdparty/dist/saneyaml-0.6.1-py3-none-any.whl.ABOUT b/thirdparty/dist/saneyaml-0.6.1-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..e114b127 --- /dev/null +++ b/thirdparty/dist/saneyaml-0.6.1-py3-none-any.whl.ABOUT @@ -0,0 +1,15 @@ +about_resource: saneyaml-0.6.1-py3-none-any.whl +name: saneyaml +version: 0.6.1 +download_url: https://files.pythonhosted.org/packages/ea/c0/b41733920cef3d87ee7d1fd5a618c7bb5240ba80dd2f29c73ec3416b3e04/saneyaml-0.6.1-py3-none-any.whl +package_url: pkg:pypi/saneyaml@0.6.1 +license_expression: apache-2.0 +copyright: Copyright saneyaml project contributors +attribute: yes +track_changes: yes +checksum_md5: 47c80e3186ef3116ff2b0eb6d5c1fa71 +checksum_sha1: 34ea073247a790381a8a14e6ce3923e157187349 +licenses: + - key: apache-2.0 + name: Apache License 2.0 + file: apache-2.0.LICENSE diff --git a/thirdparty/dist/setuptools-75.8.0-py3-none-any.whl b/thirdparty/dist/setuptools-75.8.0-py3-none-any.whl deleted file mode 100644 index 65ee9ceb..00000000 Binary files a/thirdparty/dist/setuptools-75.8.0-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/setuptools-75.8.0-py3-none-any.whl.ABOUT b/thirdparty/dist/setuptools-75.8.0-py3-none-any.whl.ABOUT deleted file mode 100644 index ed9b1442..00000000 --- a/thirdparty/dist/setuptools-75.8.0-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,28 +0,0 @@ -about_resource: setuptools-75.8.0-py3-none-any.whl -name: setuptools -version: 75.8.0 -download_url: https://files.pythonhosted.org/packages/69/8a/b9dc7678803429e4a3bc9ba462fa3dd9066824d3c607490235c6a796be5a/setuptools-75.8.0-py3-none-any.whl -package_url: pkg:pypi/setuptools@75.8.0 -license_expression: apache-2.0 AND bsd-new AND lgpl-3.0 AND mit AND python -copyright: Copyright setuptools project contributors -redistribute: yes -attribute: yes -track_changes: yes -checksum_md5: ebfe74306840b1ddc42cf1c5ec127f22 -checksum_sha1: d6d3eb8d2b85b784707dcc3b48469384ca7d24a1 -licenses: - - key: apache-2.0 - name: Apache License 2.0 - file: apache-2.0.LICENSE - - key: mit - name: MIT License - file: mit.LICENSE - - key: bsd-new - name: BSD-3-Clause - file: bsd-new.LICENSE - - key: python - name: Python Software Foundation License v2 - file: python.LICENSE - - key: lgpl-3.0 - name: GNU Lesser General Public License 3.0 - file: lgpl-3.0.LICENSE diff --git a/thirdparty/dist/setuptools-82.0.0-py3-none-any.whl b/thirdparty/dist/setuptools-82.0.0-py3-none-any.whl new file mode 100644 index 00000000..5f246648 Binary files /dev/null and b/thirdparty/dist/setuptools-82.0.0-py3-none-any.whl differ diff --git a/thirdparty/dist/setuptools-82.0.0-py3-none-any.whl.ABOUT b/thirdparty/dist/setuptools-82.0.0-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..4aac63e4 --- /dev/null +++ b/thirdparty/dist/setuptools-82.0.0-py3-none-any.whl.ABOUT @@ -0,0 +1,31 @@ +about_resource: setuptools-82.0.0-py3-none-any.whl +name: setuptools +version: 82.0.0 +download_url: https://files.pythonhosted.org/packages/e1/c6/76dc613121b793286a3f91621d7b75a2b493e0390ddca50f11993eadf192/setuptools-82.0.0-py3-none-any.whl +package_url: pkg:pypi/setuptools@82.0.0 +license_expression: apache-2.0 AND bsd-new AND bsd-simplified AND lgpl-3.0 AND mit AND unknown-license-reference +copyright: Copyright setuptools project contributors +redistribute: yes +attribute: yes +track_changes: yes +checksum_md5: 46c24ad8ec6d79f6c341e76632afeb14 +checksum_sha1: 8125d8cfaf65276f0b73ed2906073f7f21b2077c +licenses: + - key: apache-2.0 + name: Apache License 2.0 + file: apache-2.0.LICENSE + - key: mit + name: MIT License + file: mit.LICENSE + - key: bsd-simplified + name: BSD-2-Clause + file: bsd-simplified.LICENSE + - key: bsd-new + name: BSD-3-Clause + file: bsd-new.LICENSE + - key: lgpl-3.0 + name: GNU Lesser General Public License 3.0 + file: lgpl-3.0.LICENSE + - key: unknown-license-reference + name: Unknown License file reference + file: unknown-license-reference.LICENSE diff --git a/thirdparty/dist/setuptools_rust-1.10.2-py3-none-any.whl b/thirdparty/dist/setuptools_rust-1.10.2-py3-none-any.whl deleted file mode 100644 index f3f878c2..00000000 Binary files a/thirdparty/dist/setuptools_rust-1.10.2-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/setuptools_rust-1.12.0-py3-none-any.whl b/thirdparty/dist/setuptools_rust-1.12.0-py3-none-any.whl new file mode 100644 index 00000000..cdf6e8a3 Binary files /dev/null and b/thirdparty/dist/setuptools_rust-1.12.0-py3-none-any.whl differ diff --git a/thirdparty/dist/setuptools_rust-1.12.0-py3-none-any.whl.ABOUT b/thirdparty/dist/setuptools_rust-1.12.0-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..a4f0d099 --- /dev/null +++ b/thirdparty/dist/setuptools_rust-1.12.0-py3-none-any.whl.ABOUT @@ -0,0 +1,14 @@ +about_resource: setuptools_rust-1.12.0-py3-none-any.whl +name: setuptools-rust +version: 1.12.0 +download_url: https://files.pythonhosted.org/packages/f9/7b/d05b1778f2d4e354d103e3421c6267d923032fefcc5ca5b7df0cb21cefd0/setuptools_rust-1.12.0-py3-none-any.whl +package_url: pkg:pypi/setuptools-rust@1.12.0 +license_expression: mit +copyright: Copyright setuptools-rust project contributors +attribute: yes +checksum_md5: 74f5c6c03aff2952130d373ae3ff0df6 +checksum_sha1: 82d6120bb40945248df563882726942ad2e2bd73 +licenses: + - key: mit + name: MIT License + file: mit.LICENSE diff --git a/thirdparty/dist/setuptools_scm-9.2.2-py3-none-any.whl b/thirdparty/dist/setuptools_scm-9.2.2-py3-none-any.whl new file mode 100644 index 00000000..f16bc639 Binary files /dev/null and b/thirdparty/dist/setuptools_scm-9.2.2-py3-none-any.whl differ diff --git a/thirdparty/dist/setuptools_scm-9.2.2-py3-none-any.whl.ABOUT b/thirdparty/dist/setuptools_scm-9.2.2-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..76509c01 --- /dev/null +++ b/thirdparty/dist/setuptools_scm-9.2.2-py3-none-any.whl.ABOUT @@ -0,0 +1,14 @@ +about_resource: setuptools_scm-9.2.2-py3-none-any.whl +name: setuptools-scm +version: 9.2.2 +download_url: https://files.pythonhosted.org/packages/3d/ea/ac2bf868899d0d2e82ef72d350d97a846110c709bacf2d968431576ca915/setuptools_scm-9.2.2-py3-none-any.whl +package_url: pkg:pypi/setuptools-scm@9.2.2 +license_expression: mit +copyright: Copyright setuptools-scm project contributors +attribute: yes +checksum_md5: 4627cbb7183ca139ee0e971c1387790e +checksum_sha1: 2097c943363e2772bc91a8f327ec18a0c5b0b1cd +licenses: + - key: mit + name: MIT License + file: mit.LICENSE diff --git a/thirdparty/dist/sphinx-8.1.3-py3-none-any.whl.ABOUT b/thirdparty/dist/sphinx-8.1.3-py3-none-any.whl.ABOUT deleted file mode 100644 index f3ca2377..00000000 --- a/thirdparty/dist/sphinx-8.1.3-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,14 +0,0 @@ -about_resource: sphinx-8.1.3-py3-none-any.whl -name: sphinx -version: 8.1.3 -download_url: https://files.pythonhosted.org/packages/26/60/1ddff83a56d33aaf6f10ec8ce84b4c007d9368b21008876fceda7e7381ef/sphinx-8.1.3-py3-none-any.whl -package_url: pkg:pypi/sphinx@8.1.3 -license_expression: bsd-new -copyright: Copyright sphinx project contributors -attribute: yes -checksum_md5: 3bcca8b62843f94429276a0bf1c90c28 -checksum_sha1: 67dc18611c44f712539585db41aaee4b0a7ec646 -licenses: - - key: bsd-new - name: BSD-3-Clause - file: bsd-new.LICENSE diff --git a/thirdparty/dist/sqlparse-0.5.3-py3-none-any.whl b/thirdparty/dist/sqlparse-0.5.3-py3-none-any.whl deleted file mode 100644 index 0ac32bae..00000000 Binary files a/thirdparty/dist/sqlparse-0.5.3-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/sqlparse-0.5.3-py3-none-any.whl.ABOUT b/thirdparty/dist/sqlparse-0.5.3-py3-none-any.whl.ABOUT deleted file mode 100644 index cf7c797b..00000000 --- a/thirdparty/dist/sqlparse-0.5.3-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,14 +0,0 @@ -about_resource: sqlparse-0.5.3-py3-none-any.whl -name: sqlparse -version: 0.5.3 -download_url: https://files.pythonhosted.org/packages/a9/5c/bfd6bd0bf979426d405cc6e71eceb8701b148b16c21d2dc3c261efc61c7b/sqlparse-0.5.3-py3-none-any.whl -package_url: pkg:pypi/sqlparse@0.5.3 -license_expression: bsd-new -copyright: Copyright sqlparse project contributors -attribute: yes -checksum_md5: 28a0ac4500fe5b9b0d7c7e593b21e6df -checksum_sha1: 1522ee6a87f0edc9bcee1513ee41d40904b4d916 -licenses: - - key: bsd-new - name: BSD-3-Clause - file: bsd-new.LICENSE diff --git a/thirdparty/dist/sqlparse-0.5.5-py3-none-any.whl b/thirdparty/dist/sqlparse-0.5.5-py3-none-any.whl new file mode 100644 index 00000000..a3150059 Binary files /dev/null and b/thirdparty/dist/sqlparse-0.5.5-py3-none-any.whl differ diff --git a/thirdparty/dist/sqlparse-0.5.5-py3-none-any.whl.ABOUT b/thirdparty/dist/sqlparse-0.5.5-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..db85b421 --- /dev/null +++ b/thirdparty/dist/sqlparse-0.5.5-py3-none-any.whl.ABOUT @@ -0,0 +1,14 @@ +about_resource: sqlparse-0.5.5-py3-none-any.whl +name: sqlparse +version: 0.5.5 +download_url: https://files.pythonhosted.org/packages/49/4b/359f28a903c13438ef59ebeee215fb25da53066db67b305c125f1c6d2a25/sqlparse-0.5.5-py3-none-any.whl +package_url: pkg:pypi/sqlparse@0.5.5 +license_expression: bsd-new +copyright: Copyright sqlparse project contributors +attribute: yes +checksum_md5: d9fc99915bab1488672498cc1368e3cf +checksum_sha1: 1412101ac307a4db2bb7b06f0ce45612edcba7e6 +licenses: + - key: bsd-new + name: BSD-3-Clause + file: bsd-new.LICENSE diff --git a/thirdparty/dist/stevedore-5.4.0-py3-none-any.whl b/thirdparty/dist/stevedore-5.4.0-py3-none-any.whl deleted file mode 100644 index 8825e5da..00000000 Binary files a/thirdparty/dist/stevedore-5.4.0-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/stevedore-5.4.0-py3-none-any.whl.ABOUT b/thirdparty/dist/stevedore-5.4.0-py3-none-any.whl.ABOUT deleted file mode 100644 index 11a0d52f..00000000 --- a/thirdparty/dist/stevedore-5.4.0-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,15 +0,0 @@ -about_resource: stevedore-5.4.0-py3-none-any.whl -name: stevedore -version: 5.4.0 -download_url: https://files.pythonhosted.org/packages/8f/73/d0091d22a65b55e8fb6aca7b3b6713b5a261dd01cec4cfd28ed127ac0cfc/stevedore-5.4.0-py3-none-any.whl -package_url: pkg:pypi/stevedore@5.4.0 -license_expression: apache-2.0 -copyright: Copyright Red Hat -attribute: yes -track_changes: yes -checksum_md5: 1e09b7fe86ace2b403e93b8b7f4a5aae -checksum_sha1: d36db01bf262a71655002773aa8677f4d11c4960 -licenses: - - key: apache-2.0 - name: Apache License 2.0 - file: apache-2.0.LICENSE diff --git a/thirdparty/dist/tblib-3.0.0-py3-none-any.whl b/thirdparty/dist/tblib-3.0.0-py3-none-any.whl deleted file mode 100644 index c2cd496a..00000000 Binary files a/thirdparty/dist/tblib-3.0.0-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/tblib-3.0.0-py3-none-any.whl.ABOUT b/thirdparty/dist/tblib-3.0.0-py3-none-any.whl.ABOUT deleted file mode 100644 index 3001e16b..00000000 --- a/thirdparty/dist/tblib-3.0.0-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,17 +0,0 @@ -about_resource: tblib-3.0.0-py3-none-any.whl -name: tblib -version: 3.0.0 -download_url: https://files.pythonhosted.org/packages/9b/87/ce70db7cae60e67851eb94e1a2127d4abb573d3866d2efd302ceb0d4d2a5/tblib-3.0.0-py3-none-any.whl -package_url: pkg:pypi/tblib@3.0.0 -license_expression: bsd-new AND bsd-simplified -copyright: Copyright tblib project contributors -attribute: yes -checksum_md5: a20cd4b91b1d1b8a58bf5bdc495dfda5 -checksum_sha1: 55c2c5e8a173ee80377e0c8d3657664d5baea624 -licenses: - - key: bsd-simplified - name: BSD-2-Clause - file: bsd-simplified.LICENSE - - key: bsd-new - name: BSD-3-Clause - file: bsd-new.LICENSE diff --git a/thirdparty/dist/tblib-3.2.2-py3-none-any.whl b/thirdparty/dist/tblib-3.2.2-py3-none-any.whl new file mode 100644 index 00000000..d2e48c8c Binary files /dev/null and b/thirdparty/dist/tblib-3.2.2-py3-none-any.whl differ diff --git a/thirdparty/dist/tblib-3.2.2-py3-none-any.whl.ABOUT b/thirdparty/dist/tblib-3.2.2-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..30fcc1ba --- /dev/null +++ b/thirdparty/dist/tblib-3.2.2-py3-none-any.whl.ABOUT @@ -0,0 +1,20 @@ +about_resource: tblib-3.2.2-py3-none-any.whl +name: tblib +version: 3.2.2 +download_url: https://files.pythonhosted.org/packages/02/be/5d2d47b1fb58943194fb59dcf222f7c4e35122ec0ffe8c36e18b5d728f0b/tblib-3.2.2-py3-none-any.whl +package_url: pkg:pypi/tblib@3.2.2 +license_expression: bsd-new AND bsd-simplified AND unknown-license-reference +copyright: Copyright tblib project contributors +attribute: yes +checksum_md5: 2a6d3dd5596c0d1f9ac7b2adb5dae07b +checksum_sha1: be31ad2c11ce3d180b3bceaeefe6487619198f43 +licenses: + - key: bsd-simplified + name: BSD-2-Clause + file: bsd-simplified.LICENSE + - key: bsd-new + name: BSD-3-Clause + file: bsd-new.LICENSE + - key: unknown-license-reference + name: Unknown License file reference + file: unknown-license-reference.LICENSE diff --git a/thirdparty/dist/toml-0.10.2-py2.py3-none-any.whl b/thirdparty/dist/toml-0.10.2-py2.py3-none-any.whl deleted file mode 100644 index 2cb8dcbd..00000000 Binary files a/thirdparty/dist/toml-0.10.2-py2.py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/toml-0.10.2-py2.py3-none-any.whl.ABOUT b/thirdparty/dist/toml-0.10.2-py2.py3-none-any.whl.ABOUT deleted file mode 100644 index 98077cfa..00000000 --- a/thirdparty/dist/toml-0.10.2-py2.py3-none-any.whl.ABOUT +++ /dev/null @@ -1,19 +0,0 @@ -about_resource: toml-0.10.2-py2.py3-none-any.whl -name: toml -version: 0.10.2 -download_url: https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl -homepage_url: https://pypi.org/project/toml/0.10.2/ -package_url: pkg:pypi/toml@0.10.2 -license_expression: mit -copyright: Copyright (c) William Pearson -notice_file: toml-0.10.2-py2.py3-none-any.whl.NOTICE -attribute: yes -owner: Andrew Gallant -owner_url: http://burntsushi.net/ -contact: github@burntsushi.net -checksum_md5: dc26cd71b80d6757139f38156a43c545 -checksum_sha1: a55ae166e643e6c7a28c16fe005efc32ee98ee76 -licenses: - - key: mit - name: MIT License - file: mit.LICENSE diff --git a/thirdparty/dist/toml-0.10.2-py2.py3-none-any.whl.NOTICE b/thirdparty/dist/toml-0.10.2-py2.py3-none-any.whl.NOTICE deleted file mode 100644 index c5f1d31b..00000000 --- a/thirdparty/dist/toml-0.10.2-py2.py3-none-any.whl.NOTICE +++ /dev/null @@ -1,5 +0,0 @@ -Licensing -========= - -This project is released under the terms of the MIT Open Source License. View -*LICENSE.txt* for more information. \ No newline at end of file diff --git a/thirdparty/dist/typing_extensions-4.12.2-py3-none-any.whl b/thirdparty/dist/typing_extensions-4.12.2-py3-none-any.whl deleted file mode 100644 index f6cc7991..00000000 Binary files a/thirdparty/dist/typing_extensions-4.12.2-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/typing_extensions-4.15.0-py3-none-any.whl b/thirdparty/dist/typing_extensions-4.15.0-py3-none-any.whl new file mode 100644 index 00000000..5fec9ca6 Binary files /dev/null and b/thirdparty/dist/typing_extensions-4.15.0-py3-none-any.whl differ diff --git a/thirdparty/dist/typing_extensions-4.15.0-py3-none-any.whl.ABOUT b/thirdparty/dist/typing_extensions-4.15.0-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..6198e934 --- /dev/null +++ b/thirdparty/dist/typing_extensions-4.15.0-py3-none-any.whl.ABOUT @@ -0,0 +1,18 @@ +about_resource: typing_extensions-4.15.0-py3-none-any.whl +name: typing-extensions +version: 4.15.0 +download_url: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl +package_url: pkg:pypi/typing-extensions@4.15.0 +license_expression: psf-2.0 AND unknown-license-reference +copyright: Copyright typing-extensions project contributors +attribute: yes +track_changes: yes +checksum_md5: 1394f56d85d87540f7907680572797e1 +checksum_sha1: 807e4d7ccad443949cbe10eae99937fd71c2b0c8 +licenses: + - key: psf-2.0 + name: PSF-2.0 + file: psf-2.0.LICENSE + - key: unknown-license-reference + name: Unknown License file reference + file: unknown-license-reference.LICENSE diff --git a/thirdparty/dist/typing_extensions-4.2.0-py3-none-any.whl b/thirdparty/dist/typing_extensions-4.2.0-py3-none-any.whl deleted file mode 100644 index 5aa359dc..00000000 Binary files a/thirdparty/dist/typing_extensions-4.2.0-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/typing_extensions-4.2.0-py3-none-any.whl.ABOUT b/thirdparty/dist/typing_extensions-4.2.0-py3-none-any.whl.ABOUT deleted file mode 100644 index 9aa15c4e..00000000 --- a/thirdparty/dist/typing_extensions-4.2.0-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,20 +0,0 @@ -about_resource: typing_extensions-4.2.0-py3-none-any.whl -name: typing-extensions -version: 4.2.0 -download_url: https://files.pythonhosted.org/packages/75/e1/932e06004039dd670c9d5e1df0cd606bf46e29a28e65d5bb28e894ea29c9/typing_extensions-4.2.0-py3-none-any.whl -homepage_url: https://pypi.org/project/typing-extensions/4.2.0/ -package_url: pkg:pypi/typing-extensions@4.2.0 -license_expression: python -copyright: Copyright (c) Python Software Foundation -notice_file: typing_extensions-4.2.0-py3-none-any.whl.NOTICE -attribute: yes -track_changes: yes -owner: Python Software Foundation (PSF) -owner_url: http://www.python.org/psf/ -contact: http://www.python.org/psf/about/#how-do-i-reach-the-psf -checksum_md5: 95fc87a08006c5249ae13b8a1c3770b9 -checksum_sha1: ff0849420e94f425818bff5d0f25e3cdfaba8601 -licenses: - - key: python - name: Python Software Foundation License v2 - file: python.LICENSE diff --git a/thirdparty/dist/typing_extensions-4.2.0-py3-none-any.whl.NOTICE b/thirdparty/dist/typing_extensions-4.2.0-py3-none-any.whl.NOTICE deleted file mode 100644 index e9ca9a2a..00000000 --- a/thirdparty/dist/typing_extensions-4.2.0-py3-none-any.whl.NOTICE +++ /dev/null @@ -1 +0,0 @@ -Classifier: License :: OSI Approved :: Python Software Foundation License \ No newline at end of file diff --git a/thirdparty/dist/typing_inspection-0.4.2-py3-none-any.whl b/thirdparty/dist/typing_inspection-0.4.2-py3-none-any.whl new file mode 100644 index 00000000..db190df9 Binary files /dev/null and b/thirdparty/dist/typing_inspection-0.4.2-py3-none-any.whl differ diff --git a/thirdparty/dist/typing_inspection-0.4.2-py3-none-any.whl.ABOUT b/thirdparty/dist/typing_inspection-0.4.2-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..794e6edd --- /dev/null +++ b/thirdparty/dist/typing_inspection-0.4.2-py3-none-any.whl.ABOUT @@ -0,0 +1,38 @@ +about_resource: typing_inspection-0.4.2-py3-none-any.whl +name: typing-inspection +version: 0.4.2 +download_url: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl +description: | + Runtime typing introspection tools + # typing-inspection + + [![CI](https://img.shields.io/github/actions/workflow/status/pydantic/typing-inspection/ci.yml?branch=main&logo=github&label=CI)](https://github.com/pydantic/typing-inspection/actions?query=event%3Apush+branch%3Amain+workflow%3ACI) + [![Coverage](https://coverage-badge.samuelcolvin.workers.dev/pydantic/typing-inspection.svg)](https://coverage-badge.samuelcolvin.workers.dev/redirect/pydantic/typing-inspection) + [![PyPI](https://img.shields.io/pypi/v/typing-inspection.svg)](https://pypi.org/project/typing-inspection/) + [![Versions](https://img.shields.io/pypi/pyversions/typing-inspection.svg)](https://github.com/pydantic/typing-inspection) + [![License](https://img.shields.io/github/license/pydantic/typing-inspection.svg)](https://github.com/pydantic/typing-inspection/blob/main/LICENSE) + [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) + + `typing-inspection` provides tools to inspect type annotations at runtime. + + ## Installation + + From [PyPI](https://pypi.org/project/typing-inspection/): + + ```bash + pip install typing-inspection + ``` + + The library can be imported from the `typing_inspection` module. +package_url: pkg:pypi/typing-inspection@0.4.2 +license_expression: mit AND unknown-license-reference +copyright: Copyright typing-inspection project contributors +attribute: yes +checksum_md5: 245304d58de21f4f0bdd15f7ea4b0ea8 +licenses: + - key: mit + name: MIT License + file: mit.LICENSE + - key: unknown-license-reference + name: Unknown License file reference + file: unknown-license-reference.LICENSE diff --git a/thirdparty/dist/uritemplate-4.1.1-py2.py3-none-any.whl b/thirdparty/dist/uritemplate-4.1.1-py2.py3-none-any.whl deleted file mode 100644 index bead64f2..00000000 Binary files a/thirdparty/dist/uritemplate-4.1.1-py2.py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/uritemplate-4.1.1-py2.py3-none-any.whl.ABOUT b/thirdparty/dist/uritemplate-4.1.1-py2.py3-none-any.whl.ABOUT deleted file mode 100644 index 7603812c..00000000 --- a/thirdparty/dist/uritemplate-4.1.1-py2.py3-none-any.whl.ABOUT +++ /dev/null @@ -1,22 +0,0 @@ -about_resource: uritemplate-4.1.1-py2.py3-none-any.whl -name: uritemplate -version: 4.1.1 -download_url: https://files.pythonhosted.org/packages/81/c0/7461b49cd25aeece13766f02ee576d1db528f1c37ce69aee300e075b485b/uritemplate-4.1.1-py2.py3-none-any.whl -homepage_url: https://pypi.org/project/uritemplate/4.1.1/ -package_url: pkg:pypi/uritemplate@4.1.1 -license_expression: bsd-new OR apache-2.0 -copyright: Copyright (c) Ian Stapleton Cordasco -notice_file: uritemplate-4.1.1-py2.py3-none-any.whl.NOTICE -attribute: yes -track_changes: yes -owner: python-hyper -owner_url: https://github.com/python-hyper -checksum_md5: a68f2279025a2610613efb2287eaf463 -checksum_sha1: d22020a5e0f0ae271165d69889c64214516f3af2 -licenses: - - key: apache-2.0 - name: Apache License 2.0 - file: apache-2.0.LICENSE - - key: bsd-new - name: BSD-3-Clause - file: bsd-new.LICENSE diff --git a/thirdparty/dist/uritemplate-4.1.1-py2.py3-none-any.whl.NOTICE b/thirdparty/dist/uritemplate-4.1.1-py2.py3-none-any.whl.NOTICE deleted file mode 100644 index 586fa96d..00000000 --- a/thirdparty/dist/uritemplate-4.1.1-py2.py3-none-any.whl.NOTICE +++ /dev/null @@ -1,3 +0,0 @@ -This software is made available under the terms of *either* of the licenses -found in LICENSE.APACHE or LICENSE.BSD. Contributions to uritemplate are -made under the terms of *both* these licenses. \ No newline at end of file diff --git a/thirdparty/dist/uritemplate-4.2.0-py3-none-any.whl b/thirdparty/dist/uritemplate-4.2.0-py3-none-any.whl new file mode 100644 index 00000000..74e30b5e Binary files /dev/null and b/thirdparty/dist/uritemplate-4.2.0-py3-none-any.whl differ diff --git a/thirdparty/dist/uritemplate-4.2.0-py3-none-any.whl.ABOUT b/thirdparty/dist/uritemplate-4.2.0-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..925693f0 --- /dev/null +++ b/thirdparty/dist/uritemplate-4.2.0-py3-none-any.whl.ABOUT @@ -0,0 +1,18 @@ +about_resource: uritemplate-4.2.0-py3-none-any.whl +name: uritemplate +version: 4.2.0 +download_url: https://files.pythonhosted.org/packages/a9/99/3ae339466c9183ea5b8ae87b34c0b897eda475d2aec2307cae60e5cd4f29/uritemplate-4.2.0-py3-none-any.whl +package_url: pkg:pypi/uritemplate@4.2.0 +license_expression: apache-2.0 AND bsd-new +copyright: Copyright uritemplate project contributors +attribute: yes +track_changes: yes +checksum_md5: 9de1bba5e7bffd8c77388ae3b62fee87 +checksum_sha1: bba19e0b8ca9998411c73a8b949fb55324b3d19b +licenses: + - key: apache-2.0 + name: Apache License 2.0 + file: apache-2.0.LICENSE + - key: bsd-new + name: BSD-3-Clause + file: bsd-new.LICENSE diff --git a/thirdparty/dist/urllib3-2.3.0-py3-none-any.whl b/thirdparty/dist/urllib3-2.3.0-py3-none-any.whl deleted file mode 100644 index cfa568f8..00000000 Binary files a/thirdparty/dist/urllib3-2.3.0-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/urllib3-2.3.0-py3-none-any.whl.ABOUT b/thirdparty/dist/urllib3-2.3.0-py3-none-any.whl.ABOUT deleted file mode 100644 index b33f23b4..00000000 --- a/thirdparty/dist/urllib3-2.3.0-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,14 +0,0 @@ -about_resource: urllib3-2.3.0-py3-none-any.whl -name: urllib3 -version: 2.3.0 -download_url: https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl -package_url: pkg:pypi/urllib3@2.3.0 -license_expression: mit -copyright: Copyright urllib3 project contributors -attribute: yes -checksum_md5: 21cc339da6ff13770c290e665a50d1a6 -checksum_sha1: 1481268457cd2aa8a0a1465c572ab31d8ec85620 -licenses: - - key: mit - name: MIT License - file: mit.LICENSE diff --git a/thirdparty/dist/urllib3-2.6.3-py3-none-any.whl b/thirdparty/dist/urllib3-2.6.3-py3-none-any.whl new file mode 100644 index 00000000..69e9ea57 Binary files /dev/null and b/thirdparty/dist/urllib3-2.6.3-py3-none-any.whl differ diff --git a/thirdparty/dist/urllib3-2.6.3-py3-none-any.whl.ABOUT b/thirdparty/dist/urllib3-2.6.3-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..7266e4d7 --- /dev/null +++ b/thirdparty/dist/urllib3-2.6.3-py3-none-any.whl.ABOUT @@ -0,0 +1,17 @@ +about_resource: urllib3-2.6.3-py3-none-any.whl +name: urllib3 +version: 2.6.3 +download_url: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl +package_url: pkg:pypi/urllib3@2.6.3 +license_expression: mit AND unknown-license-reference +copyright: Copyright urllib3 project contributors +attribute: yes +checksum_md5: 574c8593fd05938d292a624ea3f96e89 +checksum_sha1: 29ab8033a22966e6880d05b194602b49508c78a8 +licenses: + - key: mit + name: MIT License + file: mit.LICENSE + - key: unknown-license-reference + name: Unknown License file reference + file: unknown-license-reference.LICENSE diff --git a/thirdparty/dist/wheel-0.45.1-py3-none-any.whl b/thirdparty/dist/wheel-0.45.1-py3-none-any.whl deleted file mode 100644 index 589308a2..00000000 Binary files a/thirdparty/dist/wheel-0.45.1-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/wheel-0.45.1-py3-none-any.whl.ABOUT b/thirdparty/dist/wheel-0.45.1-py3-none-any.whl.ABOUT deleted file mode 100644 index bf68a241..00000000 --- a/thirdparty/dist/wheel-0.45.1-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,14 +0,0 @@ -about_resource: wheel-0.45.1-py3-none-any.whl -name: wheel -version: 0.45.1 -download_url: https://files.pythonhosted.org/packages/0b/2c/87f3254fd8ffd29e4c02732eee68a83a1d3c346ae39bc6822dcbcb697f2b/wheel-0.45.1-py3-none-any.whl -package_url: pkg:pypi/wheel@0.45.1 -license_expression: mit -copyright: Copyright wheel project contributors -attribute: yes -checksum_md5: 67835ab585e0d1522727173dd4b09eae -checksum_sha1: 2c6c56f64de6d3c413eb5b94b8496f33468f8c98 -licenses: - - key: mit - name: MIT License - file: mit.LICENSE diff --git a/thirdparty/dist/wheel-0.46.3-py3-none-any.whl b/thirdparty/dist/wheel-0.46.3-py3-none-any.whl new file mode 100644 index 00000000..f76d8340 Binary files /dev/null and b/thirdparty/dist/wheel-0.46.3-py3-none-any.whl differ diff --git a/thirdparty/dist/wheel-0.46.3-py3-none-any.whl.ABOUT b/thirdparty/dist/wheel-0.46.3-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..214e84dd --- /dev/null +++ b/thirdparty/dist/wheel-0.46.3-py3-none-any.whl.ABOUT @@ -0,0 +1,13 @@ +about_resource: wheel-0.46.3-py3-none-any.whl +name: wheel +version: 0.46.3 +download_url: https://files.pythonhosted.org/packages/87/22/b76d483683216dde3d67cba61fb2444be8d5be289bf628c13fc0fd90e5f9/wheel-0.46.3-py3-none-any.whl +package_url: pkg:pypi/wheel@0.46.3 +license_expression: unknown-license-reference +copyright: Copyright wheel project contributors +checksum_md5: 268bf133a01ffa49c152e5e3a47296da +checksum_sha1: 8587c24d65be32a081b824d914dc31935159e2b7 +licenses: + - key: unknown-license-reference + name: Unknown License file reference + file: unknown-license-reference.LICENSE diff --git a/thirdparty/dist/wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl b/thirdparty/dist/wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl deleted file mode 100644 index e78a2789..00000000 Binary files a/thirdparty/dist/wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl and /dev/null differ diff --git a/thirdparty/dist/wrapt-1.17.2-py3-none-any.whl b/thirdparty/dist/wrapt-1.17.2-py3-none-any.whl deleted file mode 100644 index 6aed2d39..00000000 Binary files a/thirdparty/dist/wrapt-1.17.2-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/wrapt-1.17.2-py3-none-any.whl.ABOUT b/thirdparty/dist/wrapt-1.17.2-py3-none-any.whl.ABOUT deleted file mode 100644 index f4d7627f..00000000 --- a/thirdparty/dist/wrapt-1.17.2-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,14 +0,0 @@ -about_resource: wrapt-1.17.2-py3-none-any.whl -name: wrapt -version: 1.17.2 -download_url: https://files.pythonhosted.org/packages/2d/82/f56956041adef78f849db6b289b282e72b55ab8045a75abad81898c28d19/wrapt-1.17.2-py3-none-any.whl -package_url: pkg:pypi/wrapt@1.17.2 -license_expression: bsd-new -copyright: Copyright wrapt project contributors -attribute: yes -checksum_md5: 9efa1f85fae13da905c5ad0f6145f37a -checksum_sha1: ca59f76c557f4ffec6cca4e1d4d6a05f52106869 -licenses: - - key: bsd-new - name: BSD-3-Clause - file: bsd-new.LICENSE diff --git a/thirdparty/dist/xlsxwriter-3.2.9-py3-none-any.whl b/thirdparty/dist/xlsxwriter-3.2.9-py3-none-any.whl new file mode 100644 index 00000000..93545ae8 Binary files /dev/null and b/thirdparty/dist/xlsxwriter-3.2.9-py3-none-any.whl differ diff --git a/thirdparty/dist/xlsxwriter-3.2.9-py3-none-any.whl.ABOUT b/thirdparty/dist/xlsxwriter-3.2.9-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..b1482076 --- /dev/null +++ b/thirdparty/dist/xlsxwriter-3.2.9-py3-none-any.whl.ABOUT @@ -0,0 +1,17 @@ +about_resource: xlsxwriter-3.2.9-py3-none-any.whl +name: xlsxwriter +version: 3.2.9 +download_url: https://files.pythonhosted.org/packages/3a/0c/3662f4a66880196a590b202f0db82d919dd2f89e99a27fadef91c4a33d41/xlsxwriter-3.2.9-py3-none-any.whl +package_url: pkg:pypi/xlsxwriter@3.2.9 +license_expression: bsd-new AND bsd-simplified +copyright: Copyright xlsxwriter project contributors +attribute: yes +checksum_md5: 8efd73a384038b67b58437f06cf26bf5 +checksum_sha1: 23ba570845b692707c07437d9e7e4da14f6f87d9 +licenses: + - key: bsd-simplified + name: BSD-2-Clause + file: bsd-simplified.LICENSE + - key: bsd-new + name: BSD-3-Clause + file: bsd-new.LICENSE diff --git a/thirdparty/dist/zipp-3.21.0-py3-none-any.whl b/thirdparty/dist/zipp-3.21.0-py3-none-any.whl deleted file mode 100644 index ccd19f08..00000000 Binary files a/thirdparty/dist/zipp-3.21.0-py3-none-any.whl and /dev/null differ diff --git a/thirdparty/dist/zipp-3.21.0-py3-none-any.whl.ABOUT b/thirdparty/dist/zipp-3.21.0-py3-none-any.whl.ABOUT deleted file mode 100644 index babf502c..00000000 --- a/thirdparty/dist/zipp-3.21.0-py3-none-any.whl.ABOUT +++ /dev/null @@ -1,14 +0,0 @@ -about_resource: zipp-3.21.0-py3-none-any.whl -name: zipp -version: 3.21.0 -download_url: https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl -package_url: pkg:pypi/zipp@3.21.0 -license_expression: mit -copyright: Copyright zipp project contributors -attribute: yes -checksum_md5: b7151ef99fdd9e649f88086423ba2cdf -checksum_sha1: b4337a7c096e7356a76c9e4ee9ec71f9786318e9 -licenses: - - key: mit - name: MIT License - file: mit.LICENSE diff --git a/thirdparty/dist/zipp-3.23.0-py3-none-any.whl b/thirdparty/dist/zipp-3.23.0-py3-none-any.whl new file mode 100644 index 00000000..4b65cb52 Binary files /dev/null and b/thirdparty/dist/zipp-3.23.0-py3-none-any.whl differ diff --git a/thirdparty/dist/zipp-3.23.0-py3-none-any.whl.ABOUT b/thirdparty/dist/zipp-3.23.0-py3-none-any.whl.ABOUT new file mode 100644 index 00000000..b6f65070 --- /dev/null +++ b/thirdparty/dist/zipp-3.23.0-py3-none-any.whl.ABOUT @@ -0,0 +1,13 @@ +about_resource: zipp-3.23.0-py3-none-any.whl +name: zipp +version: 3.23.0 +download_url: https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl +package_url: pkg:pypi/zipp@3.23.0 +license_expression: unknown-license-reference +copyright: Copyright zipp project contributors +checksum_md5: 517d3915cf2576739abd556828f45cc4 +checksum_sha1: 102ae26651fbb4c6407b4d8cc4174d1436156c25 +licenses: + - key: unknown-license-reference + name: Unknown License file reference + file: unknown-license-reference.LICENSE diff --git a/thirdparty/js/bootstrap-5.3.2-dist.zip b/thirdparty/js/bootstrap-5.3.2-dist.zip deleted file mode 100644 index 6656312e..00000000 Binary files a/thirdparty/js/bootstrap-5.3.2-dist.zip and /dev/null differ diff --git a/thirdparty/js/bootstrap-5.3.2-dist.zip.ABOUT b/thirdparty/js/bootstrap-5.3.2-dist.zip.ABOUT deleted file mode 100644 index b24d218c..00000000 --- a/thirdparty/js/bootstrap-5.3.2-dist.zip.ABOUT +++ /dev/null @@ -1,14 +0,0 @@ -about_resource: bootstrap-5.3.2-dist.zip -name: bootstrap -version: 5.3.2 -download_url: https://github.com/twbs/bootstrap/releases/download/v5.3.2/bootstrap-5.3.2-dist.zip -package_url: pkg:github/twbs/bootstrap@5.3.2?download_url=https://github.com/twbs/bootstrap/releases/download/v5.3.2/bootstrap-5.3.2-dist.zip&version_prefix=v -license_expression: mit -copyright: Copyright bootstrap project contributors -attribute: yes -checksum_md5: a941d531b4d1ae4a46f9422253b56566 -checksum_sha1: 28c1f1f2f1b897bfdfbb8e5bbe72889d42fabdf7 -licenses: - - key: mit - name: MIT License - file: mit.LICENSE diff --git a/thirdparty/js/bootstrap-5.3.8-dist.zip b/thirdparty/js/bootstrap-5.3.8-dist.zip new file mode 100644 index 00000000..88dc7ba9 Binary files /dev/null and b/thirdparty/js/bootstrap-5.3.8-dist.zip differ diff --git a/thirdparty/js/bootstrap-5.3.8-dist.zip.ABOUT b/thirdparty/js/bootstrap-5.3.8-dist.zip.ABOUT new file mode 100644 index 00000000..895e41b7 --- /dev/null +++ b/thirdparty/js/bootstrap-5.3.8-dist.zip.ABOUT @@ -0,0 +1,14 @@ +about_resource: bootstrap-5.3.8-dist.zip +name: bootstrap +version: v5.3.8 +download_url: https://github.com/twbs/bootstrap/releases/download/v5.3.8/bootstrap-5.3.8-dist.zip +package_url: pkg:github/twbs/bootstrap@v5.3.8?download_url=https://github.com/twbs/bootstrap/releases/download/v5.3.8/bootstrap-5.3.8-dist.zip +license_expression: mit +copyright: Copyright bootstrap project contributors +attribute: yes +checksum_md5: 47618f513bb53f5ed4d36a2170d3c478 +checksum_sha1: c3c6d623ef78a8ccdd688765421ac20ee36126c5 +licenses: + - key: mit + name: MIT License + file: mit.LICENSE diff --git a/vulnerabilities/api.py b/vulnerabilities/api.py index 090a2f00..b1c62f9e 100644 --- a/vulnerabilities/api.py +++ b/vulnerabilities/api.py @@ -20,8 +20,8 @@ from dje.api_custom import TabPermission from dje.filters import LastModifiedDateFilter from dje.filters import MultipleUUIDFilter -from vulnerabilities.filters import RISK_SCORE_RANGES from vulnerabilities.filters import ScoreRangeFilter +from vulnerabilities.models import RISK_SCORE_RANGES from vulnerabilities.models import Vulnerability from vulnerabilities.models import VulnerabilityAnalysis diff --git a/vulnerabilities/fetch.py b/vulnerabilities/fetch.py index 8c097567..7f286ed0 100644 --- a/vulnerabilities/fetch.py +++ b/vulnerabilities/fetch.py @@ -50,14 +50,17 @@ def fetch_from_vulnerablecode(dataspace, batch_size, update, timeout, log_func=N timeout=timeout, log_func=log_func, ) + run_time = timer() - start_time if log_func: log_func(f"+ Created {intcomma(results.get('created', 0))} vulnerabilities") log_func(f"+ Updated {intcomma(results.get('updated', 0))} vulnerabilities") log_func(f"Completed in {humanize_time(run_time)}") - dataspace.vulnerabilities_updated_at = timezone.now() - dataspace.save(update_fields=["vulnerabilities_updated_at"]) + if results: + dataspace.vulnerabilities_updated_at = timezone.now() + dataspace.save(update_fields=["vulnerabilities_updated_at"]) + log_func("Dataspace.vulnerabilities_updated_at updated") def fetch_for_packages( @@ -65,12 +68,13 @@ def fetch_for_packages( ): from product_portfolio.models import ProductPackage + results = {"created": 0, "updated": 0} + object_count = queryset.count() if object_count < 1: - return + return results vulnerablecode = VulnerableCode(dataspace) - results = {"created": 0, "updated": 0} for index, batch in enumerate(chunked_queryset(queryset, batch_size), start=1): if log_func: @@ -134,7 +138,7 @@ def create_or_update_vulnerability( if updated_fields: results["updated"] += 1 - vulnerability.add_affected_packages(affected_packages) + vulnerability.add_affected(affected_packages) return vulnerability diff --git a/vulnerabilities/filters.py b/vulnerabilities/filters.py index dd9dcc31..e63a20a2 100644 --- a/vulnerabilities/filters.py +++ b/vulnerabilities/filters.py @@ -16,16 +16,10 @@ from dje.filters import SearchFilter from dje.widgets import DropDownRightWidget from dje.widgets import SortDropDownWidget +from vulnerabilities.models import RISK_SCORE_RANGES from vulnerabilities.models import Vulnerability from vulnerabilities.models import VulnerabilityAnalysisMixin -RISK_SCORE_RANGES = { - "low": (0.1, 2.9), - "medium": (3.0, 5.9), - "high": (6.0, 7.9), - "critical": (8.0, 10.0), -} - class NullsLastOrderingFilter(django_filters.OrderingFilter): """ diff --git a/vulnerabilities/migrations/0006_vulnerability_risk_level_and_more.py b/vulnerabilities/migrations/0006_vulnerability_risk_level_and_more.py new file mode 100644 index 00000000..510b2601 --- /dev/null +++ b/vulnerabilities/migrations/0006_vulnerability_risk_level_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 6.0.3 on 2026-03-25 12:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dje', '0014_apitoken_data'), + ('vulnerabilities', '0005_vulnerabilityanalysis_is_reachable_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='vulnerability', + name='risk_level', + field=models.GeneratedField(db_persist=True, expression=models.Case(models.When(risk_score__gte=8.0, then=models.Value('critical')), models.When(risk_score__gte=6.0, then=models.Value('high')), models.When(risk_score__gte=3.0, then=models.Value('medium')), models.When(risk_score__gte=0.1, then=models.Value('low')), default=models.Value(''), output_field=models.CharField(max_length=8)), output_field=models.CharField(max_length=8)), + ), + migrations.AddIndex( + model_name='vulnerability', + index=models.Index(fields=['risk_level'], name='vulnerabili_risk_le_a21c6a_idx'), + ), + ] diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index c50d1ea1..7bf65d2f 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -11,7 +11,12 @@ from django.contrib.postgres.fields import ArrayField from django.db import models +from django.db.models import Case +from django.db.models import CharField from django.db.models import Count +from django.db.models import GeneratedField +from django.db.models import Value +from django.db.models import When from django.utils import timezone from django.utils.translation import gettext_lazy as _ @@ -27,6 +32,24 @@ logger = logging.getLogger("dje") +RISK_SCORE_RANGES = { + "low": (0.1, 2.9), + "medium": (3.0, 5.9), + "high": (6.0, 7.9), + "critical": (8.0, 10.0), +} + + +def get_risk_level(score): + """Return the risk severity level for a given risk score.""" + if score is None: + return "" + score = float(score) + for label, (low, high) in RISK_SCORE_RANGES.items(): + if low <= score <= high: + return label + return "" + class VulnerabilityQuerySet(DataspacedQuerySet): def with_affected_products_count(self): @@ -60,7 +83,7 @@ class Vulnerability(HistoryDateFieldsMixin, DataspacedModel): A software vulnerability with a unique identifier and alternate aliases. Adapted from the VulnerableCode models at - https://github.com/nexB/vulnerablecode/blob/main/vulnerabilities/models.py#L164 + https://github.com/nexB/vulnerablecode/blob/main/vulnerabilities/models.py Note that this model implements the HistoryDateFieldsMixin but not the HistoryUserFieldsMixin as the Vulnerability records are usually created @@ -148,6 +171,18 @@ class Vulnerability(HistoryDateFieldsMixin, DataspacedModel): "exploitability, capped at 10." ), ) + risk_level = GeneratedField( + expression=Case( + When(risk_score__gte=8.0, then=Value("critical")), + When(risk_score__gte=6.0, then=Value("high")), + When(risk_score__gte=3.0, then=Value("medium")), + When(risk_score__gte=0.1, then=Value("low")), + default=Value(""), + output_field=CharField(max_length=8), + ), + output_field=CharField(max_length=8), + db_persist=True, + ) objects = DataspacedManager.from_queryset(VulnerabilityQuerySet)() @@ -156,6 +191,7 @@ class Meta: unique_together = (("dataspace", "vulnerability_id"), ("dataspace", "uuid")) indexes = [ models.Index(fields=["vulnerability_id"]), + models.Index(fields=["risk_level"]), ] def __str__(self): @@ -172,34 +208,16 @@ def cve(self): return alias def add_affected(self, instances): - """ - Assign the ``instances`` (Package or Component) as affected to this - vulnerability. - """ - from component_catalog.models import Component - from component_catalog.models import Package - - if not isinstance(instances, list): + """Assign the ``instances`` (Package or Product) as affected by this vulnerability.""" + if not isinstance(instances, (list, tuple, models.QuerySet)): instances = [instances] for instance in instances: - if isinstance(instance, Package): - self.add_affected_packages([instance]) - if isinstance(instance, Component): - self.add_affected_components([instance]) - - def add_affected_packages(self, packages): - """Assign the ``packages`` as affected to this vulnerability.""" - through_defaults = {"dataspace_id": self.dataspace_id} - self.affected_packages.add(*packages, through_defaults=through_defaults) - - def add_affected_components(self, components): - """Assign the ``components`` as affected to this vulnerability.""" - through_defaults = {"dataspace_id": self.dataspace_id} - self.affected_components.add(*components, through_defaults=through_defaults) + instance.add_affected_by(vulnerability=self) @classmethod def create_from_data(cls, dataspace, data, validate=False, affecting=None): + """Create a Vulnerability from provided ``data``.""" instance = super().create_from_data(user=dataspace, data=data, validate=False) if affecting: @@ -207,6 +225,28 @@ def create_from_data(cls, dataspace, data, validate=False, affecting=None): return instance + @classmethod + def get_or_create_from_data(cls, dataspace, data, validate=False): + """Get or create a Vulnerability from provided ``data``.""" + vulnerability_qs = Vulnerability.objects.scope(dataspace) + + # Support for CycloneDX data structure + data = data.copy() + vulnerability_id = data.get("vulnerability_id") or data.pop("id", None) + if not vulnerability_id: + return + data["vulnerability_id"] = vulnerability_id + + vulnerability = vulnerability_qs.get_or_none(vulnerability_id=vulnerability_id) + if not vulnerability: + vulnerability = cls.create_from_data( + dataspace=dataspace, + data=data, + validate=validate, + ) + + return vulnerability + def as_cyclonedx(self, affected_instances, analysis=None): affects = [ cdx_vulnerability.BomTarget(ref=instance.cyclonedx_bom_ref) @@ -420,6 +460,21 @@ class Meta: def is_vulnerable(self): return self.affected_by_vulnerabilities.exists() + def update_risk_score(self): + """Calculate and save the maximum risk score from all affected vulnerabilities.""" + qs = self.affected_by_vulnerabilities.aggregate(models.Max("risk_score")) + max_score = qs["risk_score__max"] + + self.risk_score = max_score + self.save(update_fields=["risk_score"]) + return self.risk_score + + def add_affected_by(self, vulnerability): + """Add ``vulnerability`` as affecting this instance.""" + through_defaults = {"dataspace_id": self.dataspace_id} + self.affected_by_vulnerabilities.add(vulnerability, through_defaults=through_defaults) + self.update_risk_score() + def get_entry_for_package(self, vulnerablecode): if not self.package_url: return @@ -469,28 +524,28 @@ def fetch_vulnerabilities(self): self.create_vulnerabilities(vulnerabilities_data=affected_by_vulnerabilities) def create_vulnerabilities(self, vulnerabilities_data): + """Create and assign Vulnerabilities to this instance from provided vulnerabilities_data.""" from component_catalog.models import Package vulnerabilities = [] - vulnerability_qs = Vulnerability.objects.scope(self.dataspace) - for vulnerability_data in vulnerabilities_data: - vulnerability_id = vulnerability_data["vulnerability_id"] - vulnerability = vulnerability_qs.get_or_none(vulnerability_id=vulnerability_id) - if not vulnerability: - vulnerability = Vulnerability.create_from_data( - dataspace=self.dataspace, - data=vulnerability_data, - ) + vulnerability = Vulnerability.get_or_create_from_data( + dataspace=self.dataspace, + data=vulnerability_data, + ) vulnerabilities.append(vulnerability) - through_defaults = {"dataspace_id": self.dataspace_id} - self.affected_by_vulnerabilities.add(*vulnerabilities, through_defaults=through_defaults) + self.affected_by_vulnerabilities.add( + *vulnerabilities, + through_defaults={"dataspace_id": self.dataspace_id}, + ) - self.update(risk_score=vulnerability_data["risk_score"]) + self.update_risk_score() if isinstance(self, Package): self.productpackages.update_weighted_risk_score() + return vulnerabilities + class VulnerabilityAnalysis( VulnerabilityAnalysisMixin, diff --git a/vulnerabilities/tests/test_fetch.py b/vulnerabilities/tests/test_fetch.py index 02878b7d..239931c2 100644 --- a/vulnerabilities/tests/test_fetch.py +++ b/vulnerabilities/tests/test_fetch.py @@ -52,11 +52,26 @@ def test_vulnerabilities_fetch_from_vulnerablecode( "+ Created 2 vulnerabilities" "+ Updated 0 vulnerabilities" "Completed in 0 seconds" + "Dataspace.vulnerabilities_updated_at updated" ) self.assertEqual(expected, buffer.getvalue()) self.dataspace.refresh_from_db() self.assertIsNotNone(self.dataspace.vulnerabilities_updated_at) + buffer = io.StringIO() + dataspace_empty = Dataspace.objects.create(name="empty") + mock_fetch_for_packages.return_value = {} + fetch_from_vulnerablecode( + dataspace_empty, batch_size=1, update=True, timeout=None, log_func=buffer.write + ) + expected = ( + "0 Packages in the queue." + "+ Created 0 vulnerabilities" + "+ Updated 0 vulnerabilities" + "Completed in 0 seconds" + ) + self.assertEqual(expected, buffer.getvalue()) + @mock.patch("dejacode_toolkit.vulnerablecode.VulnerableCode.bulk_search_by_purl") def test_vulnerabilities_fetch_for_packages(self, mock_bulk_search_by_purl): buffer = io.StringIO() diff --git a/vulnerabilities/tests/test_models.py b/vulnerabilities/tests/test_models.py index 87e528b9..e5e31159 100644 --- a/vulnerabilities/tests/test_models.py +++ b/vulnerabilities/tests/test_models.py @@ -24,6 +24,7 @@ from product_portfolio.tests import make_product_package from vulnerabilities.models import Vulnerability from vulnerabilities.models import VulnerabilityAnalysis +from vulnerabilities.models import get_risk_level from vulnerabilities.tests import make_vulnerability from vulnerabilities.tests import make_vulnerability_analysis @@ -82,19 +83,74 @@ def test_vulnerability_mixin_create_vulnerabilities(self): response_file = self.data / "vulnerabilities" / "idna_3.6_response.json" response_json = json.loads(response_file.read_text()) vulnerabilities_data = response_json["results"][0]["affected_by_vulnerabilities"] + vulnerabilities_data.append({"vulnerability_id": "VCID-0002", "risk_score": 5.0}) package1 = make_package(self.dataspace, package_url="pkg:pypi/idna@3.6") product1 = make_product(self.dataspace, inventory=[package1]) package1.create_vulnerabilities(vulnerabilities_data) - self.assertEqual(1, Vulnerability.objects.scope(self.dataspace).count()) - self.assertEqual(1, package1.affected_by_vulnerabilities.count()) - vulnerability = package1.affected_by_vulnerabilities.get() - self.assertEqual("VCID-j3au-usaz-aaag", vulnerability.vulnerability_id) - - self.assertEqual(8.4, package1.risk_score) + self.assertEqual(2, Vulnerability.objects.scope(self.dataspace).count()) + self.assertEqual("8.4", str(package1.risk_score)) self.assertEqual("8.4", str(product1.productpackages.get().weighted_risk_score)) + def test_vulnerability_mixin_update_risk_score(self): + package1 = make_package(self.dataspace) + + # Test with no vulnerabilities + package1.update_risk_score() + self.assertIsNone(package1.risk_score) + + # Test with one vulnerability with risk score + vulnerability1 = make_vulnerability(dataspace=self.dataspace, risk_score=7.5) + vulnerability1.add_affected(package1) + package1.update_risk_score() + self.assertEqual("7.5", str(package1.risk_score)) + + # Test with multiple vulnerabilities, should use max + vulnerability2 = make_vulnerability(dataspace=self.dataspace, risk_score=9.2) + vulnerability2.add_affected(package1) + package1.update_risk_score() + self.assertEqual("9.2", str(package1.risk_score)) + + # Test with vulnerability with lower risk score, should keep max + vulnerability3 = make_vulnerability(dataspace=self.dataspace, risk_score=3.1) + vulnerability3.add_affected(package1) + package1.update_risk_score() + self.assertEqual("9.2", str(package1.risk_score)) + + # Test with all vulnerabilities having NULL risk scores + package2 = make_package(self.dataspace) + vulnerability4 = make_vulnerability(dataspace=self.dataspace, risk_score=None) + vulnerability5 = make_vulnerability(dataspace=self.dataspace, risk_score=None) + vulnerability4.add_affected(package2) + vulnerability5.add_affected(package2) + package2.update_risk_score() + self.assertIsNone(package2.risk_score) + + def test_vulnerability_mixin_add_affected_by(self): + package1 = make_package(self.dataspace) + + vulnerability1 = make_vulnerability(self.dataspace, risk_score=1.0) + vulnerability2 = make_vulnerability(self.dataspace, risk_score=10.0) + vulnerability3 = make_vulnerability(self.dataspace, risk_score=5.0) + + package1.add_affected_by(vulnerability1) + package1.refresh_from_db() + self.assertEqual("1.0", str(package1.risk_score)) + + package1.add_affected_by(vulnerability2) + package1.refresh_from_db() + self.assertEqual("10.0", str(package1.risk_score)) + + package1.add_affected_by(vulnerability3) + package1.refresh_from_db() + self.assertEqual("10.0", str(package1.risk_score)) + + self.assertEqual(package1, vulnerability1.affected_packages.get()) + self.assertEqual(package1, vulnerability2.affected_packages.get()) + self.assertEqual(package1, vulnerability3.affected_packages.get()) + self.assertEqual(3, package1.affected_by_vulnerabilities.count()) + def test_vulnerability_model_affected_packages_m2m(self): package1 = make_package(self.dataspace) vulnerability1 = make_vulnerability(dataspace=self.dataspace, affecting=package1) @@ -175,6 +231,26 @@ def test_vulnerability_model_create_from_data(self): self.assertEqual(vulnerability_data["resource_url"], vulnerability1.resource_url) self.assertQuerySetEqual(vulnerability1.affected_packages.all(), [package1]) + def test_vulnerability_model_get_or_create_from_data(self): + vulnerability_data = { + "id": "VCID-q4q6-yfng-aaag", + "summary": "In Django 3.2 before 3.2.25, 4.2 before 4.2.11, and 5.0.", + } + + vulnerability1 = Vulnerability.get_or_create_from_data( + dataspace=self.dataspace, + data=vulnerability_data, + ) + self.assertEqual(vulnerability_data["id"], vulnerability1.vulnerability_id) + self.assertEqual(vulnerability_data["summary"], vulnerability1.summary) + + vulnerability_data["vulnerability_id"] = vulnerability_data["id"] + vulnerability2 = Vulnerability.get_or_create_from_data( + dataspace=self.dataspace, + data=vulnerability_data, + ) + self.assertEqual(vulnerability1.id, vulnerability2.id) + def test_vulnerability_model_queryset_count_methods(self): package1 = make_package(self.dataspace) package2 = make_package(self.dataspace) @@ -294,3 +370,66 @@ def test_vulnerability_model_vulnerability_propagate(self): analysis1.update(state=VulnerabilityAnalysis.State.EXPLOITABLE) new_analysis = analysis1.propagate(product2.uuid, self.super_user) self.assertEqual(VulnerabilityAnalysis.State.EXPLOITABLE, new_analysis.state) + + def test_vulnerability_model_get_risk_level(self): + self.assertEqual("", get_risk_level(None)) + self.assertEqual("", get_risk_level(0.0)) + self.assertEqual("low", get_risk_level(0.1)) + self.assertEqual("low", get_risk_level(2.9)) + self.assertEqual("medium", get_risk_level(3.0)) + self.assertEqual("medium", get_risk_level(5.9)) + self.assertEqual("high", get_risk_level(6.0)) + self.assertEqual("high", get_risk_level(7.9)) + self.assertEqual("critical", get_risk_level(8.0)) + self.assertEqual("critical", get_risk_level(10.0)) + self.assertEqual("", get_risk_level(10.1)) + self.assertEqual("high", get_risk_level("7.5")) + + def test_vulnerability_model_risk_level_generated_field(self): + vulnerability1 = make_vulnerability(dataspace=self.dataspace, risk_score=None) + self.assertEqual("", vulnerability1.risk_level) + + vulnerability1.risk_score = 0.0 + vulnerability1.save() + vulnerability1.refresh_from_db() + self.assertEqual("", vulnerability1.risk_level) + + vulnerability1.risk_score = 0.1 + vulnerability1.save() + vulnerability1.refresh_from_db() + self.assertEqual("low", vulnerability1.risk_level) + + vulnerability1.risk_score = 2.9 + vulnerability1.save() + vulnerability1.refresh_from_db() + self.assertEqual("low", vulnerability1.risk_level) + + vulnerability1.risk_score = 3.0 + vulnerability1.save() + vulnerability1.refresh_from_db() + self.assertEqual("medium", vulnerability1.risk_level) + + vulnerability1.risk_score = 5.9 + vulnerability1.save() + vulnerability1.refresh_from_db() + self.assertEqual("medium", vulnerability1.risk_level) + + vulnerability1.risk_score = 6.0 + vulnerability1.save() + vulnerability1.refresh_from_db() + self.assertEqual("high", vulnerability1.risk_level) + + vulnerability1.risk_score = 7.9 + vulnerability1.save() + vulnerability1.refresh_from_db() + self.assertEqual("high", vulnerability1.risk_level) + + vulnerability1.risk_score = 8.0 + vulnerability1.save() + vulnerability1.refresh_from_db() + self.assertEqual("critical", vulnerability1.risk_level) + + vulnerability1.risk_score = 10.0 + vulnerability1.save() + vulnerability1.refresh_from_db() + self.assertEqual("critical", vulnerability1.risk_level) diff --git a/workflow/admin.py b/workflow/admin.py index 334f7f0a..369fcc82 100644 --- a/workflow/admin.py +++ b/workflow/admin.py @@ -11,6 +11,7 @@ from django.contrib import messages from django.contrib.admin.utils import unquote from django.core.exceptions import PermissionDenied +from django.core.exceptions import ValidationError from django.template.defaultfilters import pluralize from django.utils.translation import gettext as _ @@ -18,6 +19,7 @@ from dje.admin import dejacode_site from dje.forms import DataspacedAdminForm from workflow.inlines import QuestionInline +from workflow.integrations import is_valid_issue_tracker_id from workflow.models import Priority from workflow.models import RequestTemplate @@ -56,6 +58,23 @@ class PriorityAdmin(DataspacedAdmin): save_as = False +class RequestTemplateAdminForm(DataspacedAdminForm): + def clean_issue_tracker_id(self): + issue_tracker_id = self.cleaned_data.get("issue_tracker_id") + if issue_tracker_id and not is_valid_issue_tracker_id(issue_tracker_id): + raise ValidationError( + [ + "Invalid issue tracker URL format. Supported formats include:", + "• Forgejo: https://forgejo.DOMAIN.org/OR/REPO_NAME", + "• GitHub: https://github.com/ORG/REPO_NAME", + "• GitLab: https://gitlab.com/GROUP/PROJECT_NAME", + "• Jira: https://YOUR_DOMAIN.atlassian.net/projects/PROJECTKEY", + "• SourceHut: https://todo.sr.ht/~USERNAME/PROJECT_NAME", + ] + ) + return issue_tracker_id + + @admin.register(RequestTemplate, site=dejacode_site) class RequestTemplateAdmin(DataspacedAdmin): list_display = ( @@ -75,6 +94,7 @@ class RequestTemplateAdmin(DataspacedAdmin): "include_applies_to", "include_product", ) + form = RequestTemplateAdminForm inlines = (QuestionInline,) actions = [ "copy_to", diff --git a/workflow/api.py b/workflow/api.py index e9640b1f..030fc1b4 100644 --- a/workflow/api.py +++ b/workflow/api.py @@ -14,6 +14,9 @@ import django_filters from rest_framework import serializers +from rest_framework import status +from rest_framework.decorators import action +from rest_framework.response import Response from rest_framework.viewsets import ReadOnlyModelViewSet from dje.api import CreateRetrieveUpdateListViewSet @@ -165,7 +168,12 @@ class RequestSerializer(DataspacedSerializer): ) request_template_name = serializers.StringRelatedField(source="request_template.name") requester = serializers.StringRelatedField() - assignee = DataspacedSlugRelatedField(slug_field="username") + assignee = DataspacedSlugRelatedField( + slug_field="username", + # Not required in the REST API context to simplify external integrations. + allow_null=True, + required=False, + ) priority = DataspacedSlugRelatedField( slug_field="label", allow_null=True, @@ -402,3 +410,19 @@ def perform_update(self, serializer): event_type=RequestEvent.EDIT, dataspace=self.request.user.dataspace, ) + + @action( + detail=True, + methods=["post"], + serializer_class=RequestCommentSerializer, + ) + def add_comment(self, request, *args, **kwargs): + """Add a comment to this request.""" + request_instance = self.get_object() + + serializer = RequestCommentSerializer(data=request.data) + if serializer.is_valid(): + request_instance.add_comment(self.request.user, **serializer.validated_data) + return Response({"status": "Comment added."}, status=status.HTTP_201_CREATED) + + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) diff --git a/workflow/integrations/__init__.py b/workflow/integrations/__init__.py new file mode 100644 index 00000000..bf649925 --- /dev/null +++ b/workflow/integrations/__init__.py @@ -0,0 +1,76 @@ +# +# Copyright (c) nexB Inc. and others. All rights reserved. +# DejaCode is a trademark of nexB Inc. +# SPDX-License-Identifier: AGPL-3.0-only +# See https://github.com/aboutcode-org/dejacode for support or download. +# See https://aboutcode.org for more information about AboutCode FOSS projects. +# + +import re + +from workflow.integrations.base import BaseIntegration +from workflow.integrations.forgejo import ForgejoIntegration +from workflow.integrations.github import GitHubIntegration +from workflow.integrations.gitlab import GitLabIntegration +from workflow.integrations.jira import JiraIntegration +from workflow.integrations.sourcehut import SourceHutIntegration + +__all__ = [ + "BaseIntegration", + "ForgejoIntegration", + "GitHubIntegration", + "GitLabIntegration", + "JiraIntegration", + "SourceHutIntegration", + "is_valid_issue_tracker_id", + "get_class_for_tracker", + "get_class_for_platform", +] + +FORGEJO_PATTERN = re.compile(r"^https://(?:[a-zA-Z0-9.-]*forgejo[a-zA-Z0-9.-]*)/[^/]+/[^/]+/?$") + +GITHUB_PATTERN = re.compile(r"^https://github\.com/[^/]+/[^/]+/?$") + +GITLAB_PATTERN = re.compile(r"^https://gitlab\.com/[^/]+/[^/]+/?$") + +JIRA_PATTERN = re.compile( + r"^https://[a-zA-Z0-9.-]+\.atlassian\.net(?:/[^/]+)*" + r"/(?:projects|browse)/[A-Z][A-Z0-9]+(?:/[^/]*)*/*$" +) + +SOURCEHUT_PATTERN = re.compile(r"^https://todo\.sr\.ht/~[^/]+/[^/]+/?$") + +ISSUE_TRACKER_PATTERNS = [ + FORGEJO_PATTERN, + GITHUB_PATTERN, + GITLAB_PATTERN, + JIRA_PATTERN, + SOURCEHUT_PATTERN, +] + + +def is_valid_issue_tracker_id(issue_tracker_id): + return any(pattern.match(issue_tracker_id) for pattern in ISSUE_TRACKER_PATTERNS) + + +def get_class_for_tracker(issue_tracker_id): + if "github.com" in issue_tracker_id: + return GitHubIntegration + elif "gitlab.com" in issue_tracker_id: + return GitLabIntegration + elif "atlassian.net" in issue_tracker_id: + return JiraIntegration + elif "forgejo" in issue_tracker_id: + return ForgejoIntegration + elif "todo.sr.ht" in issue_tracker_id: + return SourceHutIntegration + + +def get_class_for_platform(platform): + return { + "forgejo": ForgejoIntegration, + "github": GitHubIntegration, + "gitlab": GitLabIntegration, + "jira": JiraIntegration, + "sourcehut": SourceHutIntegration, + }.get(platform) diff --git a/workflow/integrations/base.py b/workflow/integrations/base.py new file mode 100644 index 00000000..b41adb9e --- /dev/null +++ b/workflow/integrations/base.py @@ -0,0 +1,145 @@ +# +# Copyright (c) nexB Inc. and others. All rights reserved. +# DejaCode is a trademark of nexB Inc. +# SPDX-License-Identifier: AGPL-3.0-only +# See https://github.com/aboutcode-org/dejacode for support or download. +# See https://aboutcode.org for more information about AboutCode FOSS projects. +# + +import logging + +from django.conf import settings + +import requests + +logger = logging.getLogger("dje") + +DEJACODE_SITE_URL = settings.SITE_URL.rstrip("/") + + +class BaseIntegration: + """Base class for managing issue tracker integrations from DejaCode requests.""" + + default_timeout = 10 + open_status = "open" + closed_status = "closed" + + def __init__(self, dataspace, timeout=None): + if not dataspace: + raise ValueError("Dataspace must be provided.") + self.dataspace = dataspace + self.timeout = timeout or self.default_timeout + self.session = self.get_session() + + def get_session(self): + session = requests.Session() + session.headers.update(self.get_headers()) + return session + + def get_headers(self): + """ + Return authentication headers specific to the integration. + Must be implemented in subclasses. + """ + raise NotImplementedError + + def request(self, method, url, params=None, data=None, json=None): + """Send a HTTP request.""" + try: + response = self.session.request( + method=method, + url=url, + params=params, + data=data, + json=json, + timeout=self.timeout, + ) + except requests.Timeout: + logger.warning(f"Timeout occurred during {method} request to {url}") + raise + + try: + response.raise_for_status() + except requests.HTTPError as error: + self._log_http_error(method, url, response, error) + raise + + # If there's no content (e.g. 204 No Content), return None + if response.status_code == 204: + return + + return response.json() + + def _log_http_error(self, method, url, response, error): + """Log HTTP errors with detailed information.""" + logger.error( + f"[{self.dataspace}] HTTP error during {method} request to {url}: {error}\n" + f"Response status code: {response.status_code}\n" + f"Response body: {response.text}" + ) + + def get(self, url, params=None): + """Send a GET request.""" + return self.request("GET", url, params=params) + + def post(self, url, json=None): + """Send a POST request.""" + return self.request("POST", url, json=json) + + def put(self, url, json=None): + """Send a PUT request.""" + return self.request("PUT", url, json=json) + + def patch(self, url, json=None): + """Send a PATCH request.""" + return self.request("PATCH", url, json=json) + + def sync(self, request): + """Sync the given request by creating or updating an external issue.""" + raise NotImplementedError + + def post_comment(self, repo_id, issue_id, comment_body, base_url=None): + """Post a comment on an external issue.""" + raise NotImplementedError + + def get_status(self, request): + if request.is_closed: + return self.closed_status + return self.open_status + + @staticmethod + def make_issue_title(request): + return f"[DEJACODE] {request.title}" + + @staticmethod + def make_issue_body(request): + request_url = f"{DEJACODE_SITE_URL}{request.get_absolute_url()}" + label_fields = [ + ("📝 Request Template", request.request_template), + ("📦 Product Context", request.product_context), + ("📌 Applies To", request.content_object), + ("🙋 Submitted By", request.requester), + ("👤 Assigned To", request.assignee), + ("🚨 Priority", request.priority), + ("🗒️ Notes", request.notes), + ("🔗️ DejaCode URL", request_url), + ] + + lines = [] + for label, value in label_fields: + if value: + lines.append(f"### {label}\n{value}") + + lines.append("----") + + for question in request.get_serialized_data_as_list(): + label = question.get("label") + value = question.get("value") + input_type = question.get("input_type") + + if input_type == "BooleanField": + value = "Yes" if str(value).lower() in ("1", "true", "yes") else "No" + + lines.append(f"### {label}\n{value}") + + return "\n\n".join(lines) diff --git a/workflow/integrations/forgejo.py b/workflow/integrations/forgejo.py new file mode 100644 index 00000000..519d7666 --- /dev/null +++ b/workflow/integrations/forgejo.py @@ -0,0 +1,108 @@ +# +# Copyright (c) nexB Inc. and others. All rights reserved. +# DejaCode is a trademark of nexB Inc. +# SPDX-License-Identifier: AGPL-3.0-only +# See https://github.com/aboutcode-org/dejacode for support or download. +# See https://aboutcode.org for more information about AboutCode FOSS projects. +# + +from urllib.parse import urlparse + +from workflow.integrations.base import BaseIntegration + +FORGEJO_API_PATH = "/api/v1" + + +class ForgejoIntegration(BaseIntegration): + """ + A class for managing Forgejo issue creation, updates, and comments + from DejaCode requests. + """ + + def get_headers(self): + forgejo_token = self.dataspace.get_configuration("forgejo_token") + if not forgejo_token: + raise ValueError("The forgejo_token is not set on the Dataspace.") + return {"Authorization": f"token {forgejo_token}"} + + def sync(self, request): + """Sync the given request with Forgejo by creating or updating an issue.""" + try: + base_url, repo_path = self.extract_forgejo_info( + request.request_template.issue_tracker_id + ) + except ValueError as error: + raise ValueError(f"Invalid Forgejo tracker URL: {error}") + + self.api_url = base_url.rstrip("/") + FORGEJO_API_PATH + + external_issue = request.external_issue + if external_issue: + self.update_issue( + repo_id=repo_path, + issue_id=external_issue.issue_id, + title=self.make_issue_title(request), + body=self.make_issue_body(request), + state=self.get_status(request), + ) + else: + issue = self.create_issue( + repo_id=repo_path, + title=self.make_issue_title(request), + body=self.make_issue_body(request), + ) + request.link_external_issue( + platform="forgejo", + repo=repo_path, + issue_id=issue["number"], + base_url=base_url, + ) + + def create_issue(self, repo_id, title, body=""): + """Create a new Forgejo issue.""" + url = f"{self.api_url}/repos/{repo_id}/issues" + data = { + "title": title, + "body": body, + } + + return self.post(url, json=data) + + def update_issue(self, repo_id, issue_id, title=None, body=None, state=None): + """Update an existing Forgejo issue.""" + url = f"{self.api_url}/repos/{repo_id}/issues/{issue_id}" + data = {} + if title: + data["title"] = title + if body: + data["body"] = body + if state: + data["state"] = state + + return self.patch(url, json=data) + + def post_comment(self, repo_id, issue_id, comment_body, base_url=None): + """Post a comment on an existing Forgejo issue.""" + url = f"{base_url}{FORGEJO_API_PATH}/repos/{repo_id}/issues/{issue_id}/comments" + return self.post(url, json={"body": comment_body}) + + @staticmethod + def extract_forgejo_info(url): + """ + Extract the Forgejo base domain and repo path (org/repo) from a repo URL. + + Example: + - https://forgejo.example.org/org/repo → ("https://forgejo.example.org", "org/repo") + + """ + parsed = urlparse(url) + if not parsed.netloc: + raise ValueError("Missing hostname in Forgejo URL.") + + base_url = f"{parsed.scheme}://{parsed.netloc}" + path_parts = [p for p in parsed.path.split("/") if p] + if len(path_parts) < 2: + raise ValueError("Incomplete Forgejo repository path.") + + repo_path = f"{path_parts[0]}/{path_parts[1]}" + return base_url, repo_path diff --git a/workflow/integrations/github.py b/workflow/integrations/github.py new file mode 100644 index 00000000..d5845040 --- /dev/null +++ b/workflow/integrations/github.py @@ -0,0 +1,109 @@ +# +# Copyright (c) nexB Inc. and others. All rights reserved. +# DejaCode is a trademark of nexB Inc. +# SPDX-License-Identifier: AGPL-3.0-only +# See https://github.com/aboutcode-org/dejacode for support or download. +# See https://aboutcode.org for more information about AboutCode FOSS projects. +# + +from urllib.parse import urlparse + +from workflow.integrations.base import BaseIntegration + +GITHUB_API_URL = "https://api.github.com" + + +class GitHubIntegration(BaseIntegration): + """ + A class for managing GitHub issue creation, updates, and comments + from DejaCode requests. + """ + + api_url = GITHUB_API_URL + + def get_headers(self): + github_token = self.dataspace.get_configuration(field_name="github_token") + if not github_token: + raise ValueError("The github_token is not set on the Dataspace.") + return {"Authorization": f"token {github_token}"} + + def sync(self, request): + """Sync the given request with GitHub by creating or updating an issue.""" + try: + repo_id = self.extract_github_repo_path(request.request_template.issue_tracker_id) + except ValueError as error: + raise ValueError(f"Invalid GitHub repository URL: {error}") + + labels = [] + if request.priority: + labels.append(str(request.priority)) + + external_issue = request.external_issue + if external_issue: + self.update_issue( + repo_id=repo_id, + issue_id=external_issue.issue_id, + title=self.make_issue_title(request), + body=self.make_issue_body(request), + state=self.get_status(request), + labels=labels, + ) + else: + issue = self.create_issue( + repo_id=repo_id, + title=self.make_issue_title(request), + body=self.make_issue_body(request), + labels=labels, + ) + request.link_external_issue( + platform="github", + repo=repo_id, + issue_id=issue["number"], + ) + + def create_issue(self, repo_id, title, body="", labels=None): + """Create a new GitHub issue.""" + url = f"{self.api_url}/repos/{repo_id}/issues" + data = { + "title": title, + "body": body, + } + if labels: + data["labels"] = labels + + return self.post(url, json=data) + + def update_issue(self, repo_id, issue_id, title=None, body=None, state=None, labels=None): + """Update an existing GitHub issue.""" + url = f"{self.api_url}/repos/{repo_id}/issues/{issue_id}" + data = {} + if title: + data["title"] = title + if body: + data["body"] = body + if state: + data["state"] = state + if labels: + data["labels"] = labels + + return self.patch(url, json=data) + + def post_comment(self, repo_id, issue_id, comment_body, base_url=None): + """Post a comment on an existing GitHub issue.""" + url = f"{self.api_url}/repos/{repo_id}/issues/{issue_id}/comments" + data = {"body": comment_body} + + return self.post(url, json=data) + + @staticmethod + def extract_github_repo_path(url): + """Extract 'username/repo-name' from a GitHub URL.""" + parsed = urlparse(url) + if "github.com" not in parsed.netloc: + raise ValueError("URL does not point to GitHub.") + + path_parts = [part for part in parsed.path.split("/") if part] + if len(path_parts) < 2: + raise ValueError("Incomplete GitHub repository path.") + + return f"{path_parts[0]}/{path_parts[1]}" diff --git a/workflow/integrations/gitlab.py b/workflow/integrations/gitlab.py new file mode 100644 index 00000000..5ba3e165 --- /dev/null +++ b/workflow/integrations/gitlab.py @@ -0,0 +1,119 @@ +# +# Copyright (c) nexB Inc. and others. All rights reserved. +# DejaCode is a trademark of nexB Inc. +# SPDX-License-Identifier: AGPL-3.0-only +# See https://github.com/aboutcode-org/dejacode for support or download. +# See https://aboutcode.org for more information about AboutCode FOSS projects. +# + +from urllib.parse import quote +from urllib.parse import urlparse + +from workflow.integrations.base import BaseIntegration + +GITLAB_API_URL = "https://gitlab.com/api/v4" + + +class GitLabIntegration(BaseIntegration): + """ + A class for managing GitLab issue creation, updates, and comments + from DejaCode requests. + """ + + api_url = GITLAB_API_URL + open_status = "reopen" + closed_status = "close" + + def get_headers(self): + gitlab_token = self.dataspace.get_configuration(field_name="gitlab_token") + if not gitlab_token: + raise ValueError("The gitlab_token is not set on the Dataspace.") + return {"PRIVATE-TOKEN": gitlab_token} + + def sync(self, request): + """Sync the given request with GitLab by creating or updating an issue.""" + try: + project_path = self.extract_gitlab_project_path( + request.request_template.issue_tracker_id + ) + except ValueError as error: + raise ValueError(f"Invalid GitLab project URL: {error}") + + labels = [] + if request.priority: + labels.append(str(request.priority)) + + external_issue = request.external_issue + if external_issue: + self.update_issue( + repo_id=project_path, + issue_id=external_issue.issue_id, + title=self.make_issue_title(request), + body=self.make_issue_body(request), + state_event=self.get_status(request), + labels=labels, + ) + else: + issue = self.create_issue( + repo_id=project_path, + title=self.make_issue_title(request), + body=self.make_issue_body(request), + labels=labels, + ) + request.link_external_issue( + platform="gitlab", + repo=project_path, + issue_id=issue["iid"], + ) + + def create_issue(self, repo_id, title, body="", labels=None): + """Create a new GitLab issue.""" + project_path = quote(repo_id, safe="") + url = f"{self.api_url}/projects/{project_path}/issues" + data = { + "title": title, + "description": body, + } + if labels: + # GitLab expects a comma-separated string for labels + data["labels"] = ",".join(labels) + + return self.post(url, json=data) + + def update_issue(self, repo_id, issue_id, title=None, body=None, state_event=None, labels=None): + """Update an existing GitLab issue.""" + project_path = quote(repo_id, safe="") + url = f"{self.api_url}/projects/{project_path}/issues/{issue_id}" + data = {} + if title: + data["title"] = title + if body: + data["description"] = body + if state_event: + data["state_event"] = state_event + if labels: + # GitLab expects a comma-separated string for labels + data["labels"] = ",".join(labels) + + return self.put(url, json=data) + + def post_comment(self, repo_id, issue_id, comment_body, base_url=None): + """Post a comment on an existing GitLab issue.""" + project_path = quote(repo_id, safe="") + url = f"{self.api_url}/projects/{project_path}/issues/{issue_id}/notes" + data = {"body": comment_body} + + return self.post(url, json=data) + + @staticmethod + def extract_gitlab_project_path(url): + """Extract 'namespace/project' from a GitLab URL.""" + parsed = urlparse(url) + if "gitlab.com" not in parsed.netloc: + raise ValueError("URL does not point to GitLab.") + + path_parts = [part for part in parsed.path.split("/") if part] + if len(path_parts) < 2: + raise ValueError("Incomplete GitLab project path.") + + return "/".join(path_parts[:2]) diff --git a/workflow/integrations/jira.py b/workflow/integrations/jira.py new file mode 100644 index 00000000..0d3d3156 --- /dev/null +++ b/workflow/integrations/jira.py @@ -0,0 +1,183 @@ +# +# Copyright (c) nexB Inc. and others. All rights reserved. +# DejaCode is a trademark of nexB Inc. +# SPDX-License-Identifier: AGPL-3.0-only +# See https://github.com/aboutcode-org/dejacode for support or download. +# See https://aboutcode.org for more information about AboutCode FOSS projects. +# + +import base64 +import re +from urllib.parse import urlparse + +from workflow.integrations.base import BaseIntegration + +JIRA_API_PATH = "/rest/api/3" + + +class JiraIntegration(BaseIntegration): + """ + A class for managing Jira issue creation, updates, and comments + from DejaCode requests. + """ + + issuetype = "DejaCode Request" + + def get_headers(self): + jira_user = self.dataspace.get_configuration("jira_user") + jira_token = self.dataspace.get_configuration("jira_token") + if not jira_user or not jira_token: + raise ValueError("The jira_user or jira_token is not set on the Dataspace.") + + auth = f"{jira_user}:{jira_token}" + encoded_auth = base64.b64encode(auth.encode()).decode() + return { + "Authorization": f"Basic {encoded_auth}", + "Accept": "application/json", + "Content-Type": "application/json", + } + + def sync(self, request): + """Sync the given request with Jira by creating or updating an issue.""" + try: + base_url, project_key = self.extract_jira_info( + request.request_template.issue_tracker_id + ) + except ValueError as error: + raise ValueError(f"Invalid Jira tracker URL: {error}") + + self.api_url = base_url.rstrip("/") + JIRA_API_PATH + + external_issue = request.external_issue + if external_issue: + self.update_issue( + issue_id=external_issue.issue_id, + title=self.make_issue_title(request), + body=self.make_issue_body(request), + status=self.get_status(request), + ) + else: + issue = self.create_issue( + project_key=project_key, + title=self.make_issue_title(request), + body=self.make_issue_body(request), + ) + request.link_external_issue( + platform="jira", + repo=base_url, + issue_id=issue["key"], + ) + + def create_issue(self, project_key, title, body=""): + """Create a new Jira issue.""" + url = f"{self.api_url}/issue" + data = { + "fields": { + "project": {"key": project_key}, + "summary": title, + "description": markdown_to_adf(body), + "issuetype": {"name": self.issuetype}, + } + } + return self.post(url, json=data) + + def update_issue(self, issue_id, title=None, body=None, status=None): + """Update an existing Jira issue.""" + url = f"{self.api_url}/issue/{issue_id}" + fields = {} + if title: + fields["summary"] = title + if body: + fields["description"] = markdown_to_adf(body) + + if fields: + self.put(url, json={"fields": fields}) + + # Transition (e.g., close) if status is specified + if status: + self.transition_issue(issue_id, status) + + return {"id": issue_id} + + def post_comment(self, repo_id, issue_id, comment_body, base_url=None): + """Post a comment on an existing Jira issue.""" + api_url = repo_id.rstrip("/") + JIRA_API_PATH + url = f"{api_url}/issue/{issue_id}/comment" + data = {"body": markdown_to_adf(comment_body)} + return self.post(url, json=data) + + def transition_issue(self, issue_id, target_status_name): + """Transition a Jira issue to a new status by name.""" + transitions_url = f"{self.api_url}/issue/{issue_id}/transitions" + response_json = self.get(url=transitions_url) + transitions = response_json.get("transitions", []) + + # Search for a transition name that match the `target_status_name` + for transition in transitions: + if transition["to"]["name"].lower() == target_status_name.lower(): + transition_id = transition["id"] + break + else: + raise ValueError(f"No transition found for status '{target_status_name}'") + + data = {"transition": {"id": transition_id}} + return self.post(transitions_url, json=data) + + @staticmethod + def extract_jira_info(url): + """ + Extract the base Jira URL and project key from a Jira Cloud URL. + Supports: + - https://.atlassian.net/projects/PROJECTKEY + - https://.atlassian.net/browse/PROJECTKEY + - https://.atlassian.net/jira/software/projects/PROJECTKEY/... + - https://.atlassian.net/jira/servicedesk/projects/PROJECTKEY/... + """ + parsed = urlparse(url) + if not parsed.netloc.endswith("atlassian.net"): + raise ValueError("Invalid Jira Cloud domain.") + + base_url = f"{parsed.scheme}://{parsed.netloc}" + path = parsed.path + + project_key_pattern = r"/(?:[^/]+/)*(?:projects|browse)/([A-Z][A-Z0-9]+)" + match = re.search(project_key_pattern, path) + if match: + return base_url, match.group(1) + + raise ValueError("Unable to extract Jira project key from URL.") + + +def markdown_to_adf(markdown_text): + """ + Convert minimal Markdown to Atlassian Document Format (ADF). + Converts: + - '### ' headings into ADF heading blocks (level 3) + - All other non-empty lines into paragraphs + """ + lines = markdown_text.splitlines() + content = [] + + for line in lines: + stripped = line.strip() + if stripped.startswith("### "): + content.append( + { + "type": "heading", + "attrs": {"level": 3}, + "content": [{"type": "text", "text": stripped[4:].strip()}], + } + ) + elif stripped: + content.append( + { + "type": "paragraph", + "content": [{"type": "text", "text": stripped}], + } + ) + + return { + "version": 1, + "type": "doc", + "content": content, + } diff --git a/workflow/integrations/sourcehut.py b/workflow/integrations/sourcehut.py new file mode 100644 index 00000000..deb87583 --- /dev/null +++ b/workflow/integrations/sourcehut.py @@ -0,0 +1,213 @@ +# +# Copyright (c) nexB Inc. and others. All rights reserved. +# DejaCode is a trademark of nexB Inc. +# SPDX-License-Identifier: AGPL-3.0-only +# See https://github.com/aboutcode-org/dejacode for support or download. +# See https://aboutcode.org for more information about AboutCode FOSS projects. +# + +from urllib.parse import urlparse + +from workflow.integrations.base import BaseIntegration + +SOURCEHUT_GRAPHQL_API_URL = "https://todo.sr.ht/query" + + +class SourceHutIntegration(BaseIntegration): + """A SourceHut integration using GraphQL for issue creation and updates.""" + + api_url = SOURCEHUT_GRAPHQL_API_URL + open_status = "REPORTED" + closed_status = "RESOLVED" + + def get_headers(self): + token = self.dataspace.get_configuration(field_name="sourcehut_token") + if not token: + raise ValueError("The sourcehut_token is not set on the Dataspace.") + return {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + + def sync(self, request): + """Sync the given request with SourceHut by creating or updating a ticket.""" + try: + project = self.extract_sourcehut_project(request.request_template.issue_tracker_id) + except ValueError as error: + raise ValueError(f"Invalid SourceHut project URL: {error}") + + external_issue = request.external_issue + + if external_issue: + self.update_issue( + repo_id=external_issue.repo, + issue_id=external_issue.issue_id, + title=self.make_issue_title(request), + body=self.make_issue_body(request), + state=self.get_status(request), + ) + else: + issue = self.create_issue( + repo_id=project, + title=self.make_issue_title(request), + body=self.make_issue_body(request), + ) + request.link_external_issue( + platform="sourcehut", + repo=project, + issue_id=issue["id"], + ) + + def get_tracker_id(self, project): + """Return the tracker ID for a SourceHut project name (e.g., "my-project").""" + query = """ + query { + trackers { + results { + id + name + } + } + } + """ + response = self.post(self.api_url, json={"query": query}) + + try: + trackers = response["data"]["trackers"]["results"] + for tracker in trackers: + if tracker["name"] == project.split("/")[-1]: + return tracker["id"] + raise ValueError(f"No tracker found with name: {project}") + except (KeyError, TypeError): + raise ValueError("Could not retrieve tracker list from SourceHut.") + + def create_issue(self, repo_id, title, body=""): + """Create a new SourceHut ticket via GraphQL.""" + mutation = """ + mutation SubmitTicket($trackerId: Int!, $input: SubmitTicketInput!) { + submitTicket(trackerId: $trackerId, input: $input) { + id + subject + } + } + """ + variables = { + "trackerId": self.get_tracker_id(repo_id), + "input": {"subject": title, "body": body}, + } + response = self.post( + self.api_url, + json={"query": mutation, "variables": variables}, + ) + return response.get("data", {}).get("submitTicket") + + def update_issue(self, repo_id, issue_id, title=None, body=None, state=None): + """Update an existing SourceHut ticket via GraphQL.""" + mutation = """ + mutation UpdateTicket($trackerId: Int!, $ticketId: Int!, $input: UpdateTicketInput!) { + updateTicket(trackerId: $trackerId, ticketId: $ticketId, input: $input) { + id + subject + body + status + resolution + } + } + """ + input_data = {} + if title: + input_data["subject"] = title + if body: + input_data["body"] = body + + if not input_data: + raise ValueError("At least one of 'title' or 'body' must be provided.") + + tracker_id = self.get_tracker_id(repo_id) + ticket_id = int(issue_id) + + variables = { + "trackerId": tracker_id, + "ticketId": ticket_id, + "input": input_data, + } + + response = self.post( + self.api_url, + json={"query": mutation, "variables": variables}, + ) + + updated_ticket = response.get("data", {}).get("updateTicket") + + if state and state != updated_ticket.get("status"): + self.update_ticket_status(tracker_id, ticket_id, state) + + return updated_ticket + + def update_ticket_status(self, tracker_id, ticket_id, status): + """Update the status or resolution of a SourceHut ticket via GraphQL.""" + mutation = """ + mutation UpdateTicketStatus($trackerId: Int!, $ticketId: Int!, $input: UpdateStatusInput!) { + updateTicketStatus(trackerId: $trackerId, ticketId: $ticketId, input: $input) { + id + ticket { + id + subject + status + resolution + } + } + } + """ + input_data = {"status": status} + + if status == self.closed_status: + input_data["resolution"] = "CLOSED" + + variables = { + "trackerId": tracker_id, + "ticketId": int(ticket_id), + "input": input_data, + } + + response = self.post( + self.api_url, + json={"query": mutation, "variables": variables}, + ) + return response.get("data", {}).get("updateTicketStatus") + + def post_comment(self, repo_id, issue_id, comment_body, base_url=None): + """Post a comment on an existing SourceHut ticket.""" + mutation = """ + mutation SubmitComment($trackerId: Int!, $ticketId: Int!, $input: SubmitCommentInput!) { + submitComment(trackerId: $trackerId, ticketId: $ticketId, input: $input) { + id + created + ticket { + id + subject + } + } + } + """ + variables = { + "trackerId": self.get_tracker_id(repo_id), + "ticketId": int(issue_id), + "input": {"text": comment_body}, + } + + response = self.post( + self.api_url, + json={"query": mutation, "variables": variables}, + ) + return response.get("data", {}).get("submitComment") + + @staticmethod + def extract_sourcehut_project(url): + """Extract the project slug (e.g., ~user/project-name) from a SourceHut URL.""" + parsed = urlparse(url) + if "todo.sr.ht" not in parsed.netloc: + raise ValueError("URL does not point to SourceHut's todo system.") + + path_parts = [p for p in parsed.path.split("/") if p] + if len(path_parts) < 2 or not path_parts[0].startswith("~"): + raise ValueError("Invalid SourceHut path format. Expected: ~user/project") + + return f"{path_parts[0]}/{path_parts[1]}" diff --git a/workflow/migrations/0002_requesttemplate_issue_tracker_id_externalissuelink_and_more.py b/workflow/migrations/0002_requesttemplate_issue_tracker_id_externalissuelink_and_more.py new file mode 100644 index 00000000..b49a5609 --- /dev/null +++ b/workflow/migrations/0002_requesttemplate_issue_tracker_id_externalissuelink_and_more.py @@ -0,0 +1,40 @@ +# Generated by Django 5.2.4 on 2025-07-30 07:04 + +import django.db.models.deletion +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dje', '0008_dataspaceconfiguration_github_token'), + ('workflow', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='requesttemplate', + name='issue_tracker_id', + field=models.CharField(blank=True, help_text='Link to associated issue in a tracking application, provided by the integration when the issue is created.', max_length=1000, verbose_name='Issue Tracker ID'), + ), + migrations.CreateModel( + name='ExternalIssueLink', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, verbose_name='UUID')), + ('platform', models.CharField(choices=[('github', 'GitHub'), ('gitlab', 'GitLab'), ('jira', 'Jira'), ('sourcehut', 'SourceHut'), ('forgejo', 'Forgejo')], help_text='External issue tracking platform.', max_length=20)), + ('repo', models.CharField(help_text="Repository or project identifier (e.g., 'user/repo-name').", max_length=100)), + ('issue_id', models.CharField(help_text='ID or key of the issue on the external platform.', max_length=100)), + ('dataspace', models.ForeignKey(editable=False, help_text='A Dataspace is an independent, exclusive set of DejaCode data, which can be either nexB master reference data or installation-specific data.', on_delete=django.db.models.deletion.PROTECT, to='dje.dataspace')), + ], + options={ + 'unique_together': {('dataspace', 'platform', 'repo', 'issue_id'), ('dataspace', 'uuid')}, + }, + ), + migrations.AddField( + model_name='request', + name='external_issue', + field=models.ForeignKey(blank=True, help_text='Link to external issue (GitHub, GitLab, Jira, etc.)', null=True, on_delete=django.db.models.deletion.SET_NULL, to='workflow.externalissuelink'), + ), + ] diff --git a/workflow/migrations/0003_externalissuelink_base_url.py b/workflow/migrations/0003_externalissuelink_base_url.py new file mode 100644 index 00000000..7a9d8cc4 --- /dev/null +++ b/workflow/migrations/0003_externalissuelink_base_url.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.4 on 2025-08-07 14:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('workflow', '0002_requesttemplate_issue_tracker_id_externalissuelink_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='externalissuelink', + name='base_url', + field=models.URLField(blank=True, help_text='Base URL of the external issue tracker platform (e.g., https://forgejo.example.org). Used to construct API endpoints for integrations like Forgejo or Jira.', max_length=255, null=True), + ), + ] diff --git a/workflow/models.py b/workflow/models.py index 6c46c114..b861cba0 100644 --- a/workflow/models.py +++ b/workflow/models.py @@ -22,8 +22,7 @@ from django.utils import timezone from django.utils.functional import cached_property from django.utils.html import escape -from django.utils.html import format_html -from django.utils.safestring import mark_safe +from django.utils.html import mark_safe from django.utils.translation import gettext_lazy as _ import markdown @@ -39,6 +38,7 @@ from dje.models import HistoryDateFieldsMixin from dje.models import HistoryFieldsMixin from dje.models import get_unsecured_manager +from workflow import integrations from workflow.notification import request_comment_slack_payload from workflow.notification import request_slack_payload @@ -88,6 +88,76 @@ def __str__(self): return self.label +class ExternalIssueLink(DataspacedModel): + class Platform(models.TextChoices): + GITHUB = "github", _("GitHub") + GITLAB = "gitlab", _("GitLab") + JIRA = "jira", _("Jira") + SOURCEHUT = "sourcehut", _("SourceHut") + FORGEJO = "forgejo", _("Forgejo") + + platform = models.CharField( + max_length=20, + choices=Platform.choices, + help_text=_("External issue tracking platform."), + ) + + repo = models.CharField( + max_length=100, + help_text=_("Repository or project identifier (e.g., 'user/repo-name')."), + ) + + issue_id = models.CharField( + max_length=100, + help_text=_("ID or key of the issue on the external platform."), + ) + + base_url = models.URLField( + max_length=255, + blank=True, + null=True, + help_text=_( + "Base URL of the external issue tracker platform (e.g., https://forgejo.example.org). " + "Used to construct API endpoints for integrations like Forgejo or Jira." + ), + ) + + class Meta: + unique_together = ( + ("dataspace", "platform", "repo", "issue_id"), + ("dataspace", "uuid"), + ) + + def __str__(self): + return f"{self.get_platform_display()}:{self.repo}#{self.issue_id}" + + @property + def issue_url(self): + if self.platform == self.Platform.GITHUB: + return f"https://github.com/{self.repo}/issues/{self.issue_id}" + elif self.platform == self.Platform.GITLAB: + return f"https://gitlab.com/{self.repo}/-/issues/{self.issue_id}" + elif self.platform == self.Platform.JIRA: + return f"{self.repo}/browse/{self.issue_id}" + elif self.platform == self.Platform.FORGEJO: + return f"{self.base_url}/{self.repo}/issues/{self.issue_id}" + elif self.platform == self.Platform.SOURCEHUT: + return f"https://todo.sr.ht/{self.repo}/{self.issue_id}" + + @property + def icon_css_class(self): + platform_icons = { + self.Platform.GITHUB: "fa-brands fa-github", + self.Platform.GITLAB: "fa-brands fa-gitlab", + self.Platform.JIRA: "fa-brands fa-jira", + } + return platform_icons.get(self.platform, "fa-solid fa-square-up-right") + + @property + def integration_class(self): + return integrations.get_class_for_platform(self.platform) + + class RequestQuerySet(DataspacedQuerySet): BASE_SELECT_RELATED = [ "request_template", @@ -95,6 +165,7 @@ class RequestQuerySet(DataspacedQuerySet): "assignee", "priority", "product_context", + "external_issue", "last_modified_by", ] @@ -348,6 +419,14 @@ class Status(models.TextChoices): ), ) + external_issue = models.ForeignKey( + to="workflow.ExternalIssueLink", + on_delete=models.SET_NULL, + null=True, + blank=True, + help_text=_("Link to external issue (GitHub, GitLab, Jira, etc.)"), + ) + last_modified_by = LastModifiedByField() objects = DataspacedManager.from_queryset(RequestQuerySet)() @@ -378,8 +457,8 @@ def save(self, *args, **kwargs): # `previous_object_id` logic is only required on edition. previous_object_id = None - is_addition = self.pk - if is_addition: + is_change = self.pk + if is_change: previous_object_id = self.__class__.objects.get(pk=self.pk).object_id super().save(*args, **kwargs) @@ -397,6 +476,8 @@ def save(self, *args, **kwargs): return previous_object.update_request_count() + self.handle_integrations() + def get_absolute_url(self): return reverse("workflow:request_details", args=[self.uuid]) @@ -447,7 +528,7 @@ def get_serialized_data_as_html(self, html_template="{label}: {value}", separato else: serialized_data.append(line) - return format_html(separator.join(serialized_data)) + return mark_safe(separator.join(serialized_data)) @property def serialized_data_html(self): @@ -520,6 +601,61 @@ def serialize_hook(self, hook): "data": serializer.data, } + def add_comment(self, user, text): + """Create and return a RequestComment for this Request.""" + return self.comments.create( + user=user, + text=text, + dataspace=self.dataspace, + ) + + def close(self, user, reason): + """ + Set the Request status to CLOSED. + A RequestEvent is created and returned. + """ + self.status = self.Status.CLOSED + self.last_modified_by = user + self.save() + event_instance = self.events.create( + user=user, + text=reason, + event_type=RequestEvent.CLOSED, + dataspace=self.dataspace, + ) + return event_instance + + def link_external_issue(self, platform, repo, issue_id, base_url=None): + """Create or return an ExternalIssueLink associated with this Request.""" + if self.external_issue: + return self.external_issue + + if base_url: + base_url = base_url.rstrip("/") + + external_issue = ExternalIssueLink.objects.create( + dataspace=self.dataspace, + platform=platform, + repo=repo, + issue_id=str(issue_id), + base_url=base_url, + ) + + # Set the external_issue on this instance without triggering the whole + # save() + handle_integrations() logic. + self.raw_update(external_issue=external_issue) + + return external_issue + + def handle_integrations(self): + issue_tracker_id = self.request_template.issue_tracker_id + if not issue_tracker_id: + return + + integration_class = integrations.get_class_for_tracker(issue_tracker_id) + if integration_class: + integration_class(dataspace=self.dataspace).sync(request=self) + @receiver(models.signals.post_delete, sender=Request) def update_request_count_on_delete(sender, instance=None, **kwargs): @@ -545,6 +681,16 @@ class AbstractRequestEvent(HistoryDateFieldsMixin, DataspacedModel): class Meta: abstract = True + def save(self, *args, **kwargs): + """Call the handle_integrations method on save, only for addition.""" + is_addition = not self.pk + super().save(*args, **kwargs) + if is_addition: + self.handle_integrations() + + def handle_integrations(self): + pass + class RequestEvent(AbstractRequestEvent): request = models.ForeignKey( @@ -574,6 +720,22 @@ class Meta: def __str__(self): return f"{self.get_event_type_display()} by {self.user.username}" + def handle_integrations(self): + external_issue = self.request.external_issue + if not external_issue: + return + + if not self.event_type == self.CLOSED: + return + + if integration_class := external_issue.integration_class: + integration_class(dataspace=self.dataspace).post_comment( + repo_id=external_issue.repo, + issue_id=external_issue.issue_id, + comment_body=self.text, + base_url=external_issue.base_url, + ) + class RequestComment(AbstractRequestEvent): request = models.ForeignKey( @@ -644,6 +806,19 @@ def serialize_hook(self, hook): "data": data, } + def handle_integrations(self): + external_issue = self.request.external_issue + if not external_issue: + return + + if integration_class := external_issue.integration_class: + integration_class(dataspace=self.dataspace).post_comment( + repo_id=external_issue.repo, + issue_id=external_issue.issue_id, + comment_body=self.text, + base_url=external_issue.base_url, + ) + class RequestTemplateQuerySet(DataspacedQuerySet): def actives(self): @@ -723,6 +898,16 @@ class RequestTemplate(HistoryFieldsMixin, DataspacedModel): ), ) + issue_tracker_id = models.CharField( + verbose_name=_("Issue Tracker ID"), + max_length=1000, + blank=True, + help_text=_( + "Link to associated issue in a tracking application, " + "provided by the integration when the issue is created." + ), + ) + objects = DataspacedManager.from_queryset(RequestTemplateQuerySet)() class Meta: diff --git a/workflow/templates/workflow/includes/request_list_table.html b/workflow/templates/workflow/includes/request_list_table.html index d675007b..ab5eeef6 100644 --- a/workflow/templates/workflow/includes/request_list_table.html +++ b/workflow/templates/workflow/includes/request_list_table.html @@ -30,7 +30,7 @@
        - +
        @@ -46,6 +46,15 @@ #{{ req.id }} {{ req.title }} + {% if req.external_issue %} + + {% endif %}
        {% if filter.form.request_template %} {{ req.request_template.name }} diff --git a/workflow/templates/workflow/request_details.html b/workflow/templates/workflow/request_details.html index 540eb7e4..6acfbfbf 100644 --- a/workflow/templates/workflow/request_details.html +++ b/workflow/templates/workflow/request_details.html @@ -22,6 +22,15 @@

        {{ request_instance }} {{ request_instance.title }}

        + {% if request_instance.external_issue %} + + {% endif %}
        diff --git a/workflow/templates/workflow/request_form.html b/workflow/templates/workflow/request_form.html index 677d996a..05b01a54 100644 --- a/workflow/templates/workflow/request_form.html +++ b/workflow/templates/workflow/request_form.html @@ -21,6 +21,15 @@

        {% if request_instance %}{# Edition #} Edit: {{ request_instance }} {{ request_instance.title }} + {% if request_instance.external_issue %} + • + + + {{ request_instance.external_issue }} + + + + {% endif %} {% else %}{# Addition #} New Request {% endif %} @@ -72,7 +81,12 @@

        {{ form.request_template.name }}

        return false; }); - $('form#workflow-request-form').dirtyForms(); + const requestForm = $('form#workflow-request-form'); + requestForm.dirtyForms(); + requestForm.on('submit', function () { + $('button').prop('disabled', true); + NEXB.displayOverlay("Saving..."); + }) }); {% endblock %} \ No newline at end of file diff --git a/workflow/tests/test_admin.py b/workflow/tests/test_admin.py index 51ec485d..37588ecc 100644 --- a/workflow/tests/test_admin.py +++ b/workflow/tests/test_admin.py @@ -139,7 +139,7 @@ def test_request_template_admin_delete_permissions(self): url = reverse("admin:workflow_requesttemplate_delete", args=[self.request_template1.id]) response = self.client.get(url) - self.assertContains(response, "

        Are you sure?

        ") + self.assertContains(response, "

        Delete

        ") # Attaching a Request to our template. Request.objects.create( diff --git a/workflow/tests/test_api.py b/workflow/tests/test_api.py index b9da8cbe..35549732 100644 --- a/workflow/tests/test_api.py +++ b/workflow/tests/test_api.py @@ -361,10 +361,16 @@ def test_api_request_endpoint_create(self): expected = { "title": ["This field is required."], "request_template": ["This field may not be null."], - "assignee": ["This field may not be null."], } self.assertEqual(expected, response.json()) + data = { + "title": "Title", + "request_template": self.request_template1_detail_url, + } + response = self.client.post(self.request_list_url, data) + self.assertEqual(status.HTTP_201_CREATED, response.status_code) + data = { "title": "Title", "request_template": self.request_template1_detail_url, @@ -780,6 +786,24 @@ def test_api_request_endpoint_edit_serialized_data(self): expected = {"serialized_data": ['"Organization" is required.']} self.assertEqual(expected, response.data) + def test_api_request_endpoint_add_comment_action(self): + self.client.login(username="super_user", password="secret") + add_comment_url = reverse("api_v2:request-add-comment", args=[self.request1.uuid]) + + data = {} + response = self.client.post(add_comment_url, data=data) + self.assertEqual(status.HTTP_400_BAD_REQUEST, response.status_code) + expected = { + "text": ["This field is required."], + } + self.assertEqual(expected, response.json()) + + data = {"text": "Comment content."} + response = self.client.post(add_comment_url, data=data) + self.assertEqual(status.HTTP_201_CREATED, response.status_code) + expected = {"status": "Comment added."} + self.assertEqual(expected, response.data) + def test_api_request_and_request_template_endpoints_tab_permission(self): self.assertEqual((TabPermission,), RequestViewSet.extra_permissions) self.assertEqual((TabPermission,), RequestTemplateViewSet.extra_permissions) diff --git a/workflow/tests/test_integrations.py b/workflow/tests/test_integrations.py new file mode 100644 index 00000000..9d682984 --- /dev/null +++ b/workflow/tests/test_integrations.py @@ -0,0 +1,691 @@ +# +# Copyright (c) nexB Inc. and others. All rights reserved. +# DejaCode is a trademark of nexB Inc. +# SPDX-License-Identifier: AGPL-3.0-only +# See https://github.com/aboutcode-org/dejacode for support or download. +# See https://aboutcode.org for more information about AboutCode FOSS projects. +# + +import base64 +from unittest import mock +from urllib.parse import quote + +from django.contrib.contenttypes.models import ContentType +from django.test import TestCase + +from dje.models import Dataspace +from dje.tests import create_superuser +from workflow.integrations import ForgejoIntegration +from workflow.integrations import GitHubIntegration +from workflow.integrations import GitLabIntegration +from workflow.integrations import JiraIntegration +from workflow.integrations import SourceHutIntegration +from workflow.integrations import get_class_for_platform +from workflow.integrations import get_class_for_tracker +from workflow.integrations import is_valid_issue_tracker_id +from workflow.models import Question +from workflow.models import RequestTemplate + + +class WorkflowIntegrationsTestCase(TestCase): + def test_integrations_is_valid_issue_tracker_id(self): + valid_urls = [ + # GitHub + "https://github.com/org/repo", + # GitLab + "https://gitlab.com/group/project", + # Jira + "https://example.atlassian.net/browse/PROJ", + "https://example.atlassian.net/projects/PROJ", + "https://example.atlassian.net/projects/PROJ/", + "https://example.atlassian.net/projects/PROJ/summary", + "https://example.atlassian.net/jira/software/projects/PROJ", + "https://example.atlassian.net/jira/software/projects/PROJ/", + "https://example.atlassian.net/jira/software/projects/PROJ/summary", + "https://example.atlassian.net/jira/servicedesk/projects/PROJ", + # Forgejo + "https://code.forgejo.org/user/repo", + "https://git.forgejo.dev/org/project/", + "https://forgejo.example.org/team/repo", + # SourceHut + "https://todo.sr.ht/~username/project-name", + ] + for url in valid_urls: + self.assertTrue(is_valid_issue_tracker_id(url), msg=url) + + invalid_urls = [ + "https://bitbucket.org/team/repo", + "https://github.com/", + "https://gitlab.com/", + "https://atlassian.net/projects/", + "https://todo.sr.ht/", + "https://example.com", + "https://example.org/user/repo", + ] + for url in invalid_urls: + self.assertFalse(is_valid_issue_tracker_id(url), msg=url) + + def test_integrations_get_class_for_tracker(self): + self.assertIs(get_class_for_tracker("https://github.com/org/repo"), GitHubIntegration) + self.assertIs(get_class_for_tracker("https://gitlab.com/group/project"), GitLabIntegration) + self.assertIs( + get_class_for_tracker("https://example.atlassian.net/projects/PROJ"), JiraIntegration + ) + self.assertIs( + get_class_for_tracker("https://code.forgejo.org/user/repo"), ForgejoIntegration + ) + self.assertIs( + get_class_for_tracker("https://todo.sr.ht/~username/project-name"), SourceHutIntegration + ) + self.assertIsNone(get_class_for_tracker("https://example.com")) + + def test_integrations_get_class_for_platform(self): + self.assertIs(get_class_for_platform("github"), GitHubIntegration) + self.assertIs(get_class_for_platform("gitlab"), GitLabIntegration) + self.assertIs(get_class_for_platform("jira"), JiraIntegration) + self.assertIs(get_class_for_platform("forgejo"), ForgejoIntegration) + self.assertIs(get_class_for_platform("sourcehut"), SourceHutIntegration) + self.assertIsNone(get_class_for_platform("example")) + + +class GitHubIntegrationTestCase(TestCase): + def setUp(self): + patcher = mock.patch("workflow.models.Request.handle_integrations", return_value=None) + self.mock_handle_integrations = patcher.start() + self.addCleanup(patcher.stop) + + self.dataspace = Dataspace.objects.create(name="nexB") + self.dataspace.set_configuration("github_token", "fake-token") + self.super_user = create_superuser("nexb_user", self.dataspace) + self.component_ct = ContentType.objects.get( + app_label="component_catalog", model="component" + ) + self.request_template = RequestTemplate.objects.create( + name="GitHub Template", + description="Integration test template", + content_type=self.component_ct, + dataspace=self.dataspace, + issue_tracker_id="https://github.com/nexB/repo", + ) + self.question = Question.objects.create( + template=self.request_template, + label="Example Question", + input_type="TextField", + position=0, + dataspace=self.dataspace, + ) + self.request = self.request_template.create_request( + title="Example Request", + requester=self.super_user, + serialized_data='{"Example Question": "Some value"}', + ) + self.github = GitHubIntegration(dataspace=self.dataspace) + + def test_github_extract_github_repo_path_valid_url(self): + url = "https://github.com/user/repo" + result = GitHubIntegration.extract_github_repo_path(url) + self.assertEqual(result, "user/repo") + + def test_github_extract_github_repo_path_invalid_url(self): + with self.assertRaises(ValueError): + GitHubIntegration.extract_github_repo_path("https://example.com/user/repo") + + def test_github_get_headers_returns_auth_header(self): + headers = self.github.get_headers() + self.assertEqual(headers, {"Authorization": "token fake-token"}) + + def test_github_make_issue_title(self): + title = GitHubIntegration.make_issue_title(self.request) + self.assertEqual(title, "[DEJACODE] Example Request") + + def test_github_make_issue_body_contains_question(self): + body = GitHubIntegration.make_issue_body(self.request) + self.assertIn("### Example Question", body) + self.assertIn("Some value", body) + + @mock.patch("requests.Session.request") + def test_github_create_issue_calls_post(self, mock_request): + mock_request.return_value.json.return_value = {"number": 10} + mock_request.return_value.raise_for_status.return_value = None + + issue = self.github.create_issue( + repo_id="user/repo", + title="Issue Title", + body="Issue Body", + labels=["High"], + ) + + self.assertEqual(issue["number"], 10) + mock_request.assert_called_once() + + @mock.patch("requests.Session.request") + def test_github_update_issue_calls_patch(self, mock_request): + mock_request.return_value.json.return_value = {"state": "closed"} + mock_request.return_value.raise_for_status.return_value = None + + response = self.github.update_issue( + repo_id="user/repo", + issue_id=123, + title="Updated title", + body="Updated body", + state="closed", + ) + + self.assertEqual(response["state"], "closed") + mock_request.assert_called_once() + + @mock.patch("requests.Session.request") + def test_github_post_comment_calls_post(self, mock_request): + mock_request.return_value.json.return_value = {"id": 77, "body": "Test comment"} + mock_request.return_value.raise_for_status.return_value = None + + response = self.github.post_comment( + repo_id="user/repo", + issue_id=10, + comment_body="Test comment", + ) + + self.assertEqual(response["body"], "Test comment") + mock_request.assert_called_once_with( + method="POST", + url="https://api.github.com/repos/user/repo/issues/10/comments", + json={"body": "Test comment"}, + params=None, + data=None, + timeout=self.github.default_timeout, + ) + + +class GitLabIntegrationTestCase(TestCase): + def setUp(self): + patcher = mock.patch("workflow.models.Request.handle_integrations", return_value=None) + self.mock_handle_integrations = patcher.start() + self.addCleanup(patcher.stop) + + self.dataspace = Dataspace.objects.create(name="nexB") + self.dataspace.set_configuration("gitlab_token", "fake-token") + self.super_user = create_superuser("nexb_user", self.dataspace) + self.component_ct = ContentType.objects.get( + app_label="component_catalog", model="component" + ) + self.request_template = RequestTemplate.objects.create( + name="GitLab Template", + description="Integration test template", + content_type=self.component_ct, + dataspace=self.dataspace, + issue_tracker_id="https://gitlab.com/nexB/repo", + ) + self.question = Question.objects.create( + template=self.request_template, + label="Example Question", + input_type="TextField", + position=0, + dataspace=self.dataspace, + ) + self.request = self.request_template.create_request( + title="Example Request", + requester=self.super_user, + serialized_data='{"Example Question": "Some value"}', + ) + self.gitlab = GitLabIntegration(dataspace=self.dataspace) + + def test_gitlab_extract_gitlab_project_path_valid_url(self): + url = "https://gitlab.com/user/project" + result = GitLabIntegration.extract_gitlab_project_path(url) + self.assertEqual(result, "user/project") + + def test_gitlab_extract_gitlab_project_path_invalid_url(self): + with self.assertRaises(ValueError): + GitLabIntegration.extract_gitlab_project_path("https://example.com/user/project") + + def test_gitlab_get_headers_returns_auth_header(self): + headers = self.gitlab.get_headers() + self.assertEqual(headers, {"PRIVATE-TOKEN": "fake-token"}) + + def test_gitlab_make_issue_title(self): + title = self.gitlab.make_issue_title(self.request) + self.assertEqual(title, "[DEJACODE] Example Request") + + def test_gitlab_make_issue_body_contains_question(self): + body = self.gitlab.make_issue_body(self.request) + self.assertIn("### Example Question", body) + self.assertIn("Some value", body) + + @mock.patch("requests.Session.request") + def test_gitlab_create_issue_calls_post(self, mock_request): + mock_request.return_value.json.return_value = {"iid": 10} + mock_request.return_value.raise_for_status.return_value = None + + issue = self.gitlab.create_issue( + repo_id="user/project", + title="Issue Title", + body="Issue Body", + labels=["High"], + ) + + self.assertEqual(issue["iid"], 10) + mock_request.assert_called_once_with( + method="POST", + url=f"https://gitlab.com/api/v4/projects/{quote('user/project', safe='')}/issues", + params=None, + data=None, + json={ + "title": "Issue Title", + "description": "Issue Body", + "labels": "High", + }, + timeout=self.gitlab.default_timeout, + ) + + @mock.patch("requests.Session.request") + def test_gitlab_update_issue_calls_put(self, mock_request): + mock_request.return_value.json.return_value = {"state": "closed"} + mock_request.return_value.raise_for_status.return_value = None + + response = self.gitlab.update_issue( + repo_id="user/project", + issue_id=123, + title="Updated title", + body="Updated body", + state_event="close", + labels=["Urgent"], + ) + + self.assertEqual(response["state"], "closed") + + project_path = quote("user/project", safe="") + mock_request.assert_called_once_with( + method="PUT", + url=f"https://gitlab.com/api/v4/projects/{project_path}/issues/123", + params=None, + data=None, + json={ + "title": "Updated title", + "description": "Updated body", + "state_event": "close", + "labels": "Urgent", + }, + timeout=self.gitlab.default_timeout, + ) + + @mock.patch("requests.Session.request") + def test_gitlab_post_comment_calls_post(self, mock_request): + mock_request.return_value.json.return_value = {"id": 77, "body": "Test comment"} + mock_request.return_value.raise_for_status.return_value = None + + response = self.gitlab.post_comment( + repo_id="user/project", + issue_id=10, + comment_body="Test comment", + ) + + self.assertEqual(response["body"], "Test comment") + + project_path = quote("user/project", safe="") + mock_request.assert_called_once_with( + method="POST", + url=f"https://gitlab.com/api/v4/projects/{project_path}/issues/10/notes", + params=None, + data=None, + json={"body": "Test comment"}, + timeout=self.gitlab.default_timeout, + ) + + +class JiraIntegrationTestCase(TestCase): + def setUp(self): + patcher = mock.patch("workflow.models.Request.handle_integrations", return_value=None) + self.mock_handle_integrations = patcher.start() + self.addCleanup(patcher.stop) + + self.dataspace = Dataspace.objects.create(name="nexB") + self.dataspace.set_configuration("jira_user", "fake-user") + self.dataspace.set_configuration("jira_token", "fake-token") + self.super_user = create_superuser("nexb_user", self.dataspace) + self.component_ct = ContentType.objects.get( + app_label="component_catalog", model="component" + ) + self.request_template = RequestTemplate.objects.create( + name="Jira Template", + description="Integration test template", + content_type=self.component_ct, + dataspace=self.dataspace, + issue_tracker_id="https://example.atlassian.net/browse/PROJ", + ) + self.question = Question.objects.create( + template=self.request_template, + label="Example Question", + input_type="TextField", + position=0, + dataspace=self.dataspace, + ) + self.request = self.request_template.create_request( + title="Example Request", + requester=self.super_user, + serialized_data='{"Example Question": "Some value"}', + ) + self.jira = JiraIntegration(dataspace=self.dataspace) + + def test_jira_extract_jira_info_valid_urls(self): + urls = [ + "https://example.atlassian.net/browse/PROJ", + "https://example.atlassian.net/projects/PROJ", + "https://example.atlassian.net/projects/PROJ/", + "https://example.atlassian.net/projects/PROJ/summary", + "https://example.atlassian.net/jira/software/projects/PROJ", + "https://example.atlassian.net/jira/software/projects/PROJ/", + "https://example.atlassian.net/jira/software/projects/PROJ/summary", + "https://example.atlassian.net/jira/servicedesk/projects/PROJ", + ] + for url in urls: + base_url, project_key = JiraIntegration.extract_jira_info(url) + self.assertEqual(base_url, "https://example.atlassian.net") + self.assertEqual(project_key, "PROJ") + + def test_jira_extract_jira_info_invalid_url(self): + with self.assertRaises(ValueError): + JiraIntegration.extract_jira_info("https://example.com/browse/PROJ") + + def test_jira_get_headers_returns_auth_header(self): + headers = self.jira.get_headers() + expected_auth = base64.b64encode(b"fake-user:fake-token").decode() + self.assertEqual( + headers, + { + "Authorization": f"Basic {expected_auth}", + "Accept": "application/json", + "Content-Type": "application/json", + }, + ) + + def test_jira_make_issue_title(self): + title = self.jira.make_issue_title(self.request) + self.assertEqual(title, "[DEJACODE] Example Request") + + def test_jira_make_issue_body_contains_question(self): + body = self.jira.make_issue_body(self.request) + self.assertIn("### Example Question", body) + self.assertIn("Some value", body) + + @mock.patch("requests.Session.request") + def test_jira_create_issue_calls_post(self, mock_request): + mock_request.return_value.json.return_value = {"key": "PROJ-123"} + mock_request.return_value.raise_for_status.return_value = None + + self.jira.api_url = "https://example.atlassian.net/rest/api/3" + issue = self.jira.create_issue( + project_key="PROJ", + title="Issue Title", + body="Issue Body", + ) + + self.assertEqual(issue["key"], "PROJ-123") + mock_request.assert_called_once() + + @mock.patch("requests.Session.request") + def test_jira_update_issue_calls_put(self, mock_request): + mock_request.return_value.raise_for_status.return_value = None + self.jira.api_url = "https://example.atlassian.net/rest/api/3" + + response = self.jira.update_issue( + issue_id="PROJ-123", + title="Updated title", + body="Updated body", + ) + + self.assertEqual(response["id"], "PROJ-123") + mock_request.assert_called_once() + + @mock.patch("requests.Session.request") + def test_jira_post_comment_calls_post(self, mock_request): + mock_request.return_value.json.return_value = {"id": "1001"} + mock_request.return_value.raise_for_status.return_value = None + + response = self.jira.post_comment( + repo_id="https://example.atlassian.net", + issue_id="PROJ-123", + comment_body="Test comment", + ) + + self.assertEqual(response["id"], "1001") + mock_request.assert_called_once() + + +class ForgejoIntegrationTestCase(TestCase): + def setUp(self): + patcher = mock.patch("workflow.models.Request.handle_integrations", return_value=None) + self.mock_handle_integrations = patcher.start() + self.addCleanup(patcher.stop) + + self.dataspace = Dataspace.objects.create(name="nexB") + self.dataspace.set_configuration("forgejo_token", "fake-token") + self.super_user = create_superuser("nexb_user", self.dataspace) + self.component_ct = ContentType.objects.get( + app_label="component_catalog", model="component" + ) + self.request_template = RequestTemplate.objects.create( + name="Forgejo Template", + description="Integration test template", + content_type=self.component_ct, + dataspace=self.dataspace, + issue_tracker_id="https://code.forgejo.org/nexB/repo", + ) + self.question = Question.objects.create( + template=self.request_template, + label="Example Question", + input_type="TextField", + position=0, + dataspace=self.dataspace, + ) + self.request = self.request_template.create_request( + title="Example Request", + requester=self.super_user, + serialized_data='{"Example Question": "Some value"}', + ) + self.forgejo = ForgejoIntegration(dataspace=self.dataspace) + + def test_forgejo_extract_forgejo_info_valid_url(self): + url = "https://code.forgejo.org/user/repo" + base_url, repo_path = ForgejoIntegration.extract_forgejo_info(url) + self.assertEqual(base_url, "https://code.forgejo.org") + self.assertEqual(repo_path, "user/repo") + + def test_forgejo_extract_forgejo_info_invalid_url_missing_host(self): + with self.assertRaises(ValueError): + ForgejoIntegration.extract_forgejo_info("invalid-url") + + def test_forgejo_extract_forgejo_info_invalid_url_missing_repo_path(self): + with self.assertRaises(ValueError): + ForgejoIntegration.extract_forgejo_info("https://code.forgejo.org/user") + + def test_forgejo_get_headers_returns_auth_header(self): + headers = self.forgejo.get_headers() + self.assertEqual(headers, {"Authorization": "token fake-token"}) + + def test_forgejo_make_issue_title(self): + title = self.forgejo.make_issue_title(self.request) + self.assertEqual(title, "[DEJACODE] Example Request") + + def test_forgejo_make_issue_body_contains_question(self): + body = self.forgejo.make_issue_body(self.request) + self.assertIn("### Example Question", body) + self.assertIn("Some value", body) + + @mock.patch("requests.Session.request") + def test_forgejo_create_issue_calls_post(self, mock_request): + mock_request.return_value.json.return_value = {"number": 42} + mock_request.return_value.raise_for_status.return_value = None + + self.forgejo.api_url = "https://code.forgejo.org/api/v1" + issue = self.forgejo.create_issue( + repo_id="user/repo", + title="Issue Title", + body="Issue Body", + ) + self.assertEqual(issue["number"], 42) + mock_request.assert_called_once() + + @mock.patch("requests.Session.request") + def test_forgejo_update_issue_calls_patch(self, mock_request): + mock_request.return_value.json.return_value = {"state": "closed"} + mock_request.return_value.raise_for_status.return_value = None + + self.forgejo.api_url = "https://code.forgejo.org/api/v1" + response = self.forgejo.update_issue( + repo_id="user/repo", + issue_id=123, + title="Updated title", + body="Updated body", + state="closed", + ) + self.assertEqual(response["state"], "closed") + mock_request.assert_called_once() + + @mock.patch("requests.Session.request") + def test_forgejo_post_comment_calls_post(self, mock_request): + mock_request.return_value.json.return_value = {"id": 99, "body": "Test comment"} + mock_request.return_value.raise_for_status.return_value = None + + response = self.forgejo.post_comment( + repo_id="user/repo", + issue_id=123, + comment_body="Test comment", + base_url="https://code.forgejo.org", + ) + self.assertEqual(response["body"], "Test comment") + mock_request.assert_called_once_with( + method="POST", + url="https://code.forgejo.org/api/v1/repos/user/repo/issues/123/comments", + params=None, + data=None, + json={"body": "Test comment"}, + timeout=self.forgejo.default_timeout, + ) + + +class SourceHutIntegrationTestCase(TestCase): + def setUp(self): + patcher = mock.patch("workflow.models.Request.handle_integrations", return_value=None) + self.mock_handle_integrations = patcher.start() + self.addCleanup(patcher.stop) + + self.dataspace = Dataspace.objects.create(name="nexB") + self.dataspace.set_configuration("sourcehut_token", "fake-token") + self.super_user = create_superuser("nexb_user", self.dataspace) + self.component_ct = ContentType.objects.get( + app_label="component_catalog", model="component" + ) + self.request_template = RequestTemplate.objects.create( + name="SourceHut Template", + description="Integration test template", + content_type=self.component_ct, + dataspace=self.dataspace, + issue_tracker_id="https://todo.sr.ht/~username/project-name", + ) + self.question = Question.objects.create( + template=self.request_template, + label="Example Question", + input_type="TextField", + position=0, + dataspace=self.dataspace, + ) + self.request = self.request_template.create_request( + title="Example Request", + requester=self.super_user, + serialized_data='{"Example Question": "Some value"}', + ) + self.sourcehut = SourceHutIntegration(dataspace=self.dataspace) + + def test_sourcehut_extract_sourcehut_project_valid_url(self): + url = "https://todo.sr.ht/~user/project" + result = SourceHutIntegration.extract_sourcehut_project(url) + self.assertEqual(result, "~user/project") + + def test_sourcehut_extract_sourcehut_project_invalid_host(self): + with self.assertRaises(ValueError): + SourceHutIntegration.extract_sourcehut_project("https://example.com/~user/project") + + def test_sourcehut_extract_sourcehut_project_invalid_path_format(self): + with self.assertRaises(ValueError): + SourceHutIntegration.extract_sourcehut_project("https://todo.sr.ht/user/project") + + def test_sourcehut_get_headers_returns_auth_header(self): + headers = self.sourcehut.get_headers() + self.assertEqual( + headers, + { + "Authorization": "Bearer fake-token", + "Content-Type": "application/json", + }, + ) + + def test_sourcehut_make_issue_title(self): + title = self.sourcehut.make_issue_title(self.request) + self.assertEqual(title, "[DEJACODE] Example Request") + + def test_sourcehut_make_issue_body_contains_question(self): + body = self.sourcehut.make_issue_body(self.request) + self.assertIn("### Example Question", body) + self.assertIn("Some value", body) + + @mock.patch.object(SourceHutIntegration, "post") + @mock.patch.object(SourceHutIntegration, "get_tracker_id", return_value=1) + def test_sourcehut_create_issue_calls_post(self, mock_get_tracker_id, mock_post): + mock_post.return_value = {"data": {"submitTicket": {"id": 123, "subject": "Issue Title"}}} + + issue = self.sourcehut.create_issue( + repo_id="~user/project", + title="Issue Title", + body="Issue Body", + ) + + self.assertEqual(issue["id"], 123) + mock_post.assert_called_once() + mock_get_tracker_id.assert_called_once_with("~user/project") + + @mock.patch.object(SourceHutIntegration, "post") + @mock.patch.object(SourceHutIntegration, "get_tracker_id", return_value=1) + def test_sourcehut_update_issue_calls_post(self, mock_get_tracker_id, mock_post): + mock_post.return_value = { + "data": { + "updateTicket": { + "id": 123, + "subject": "Updated title", + "status": "REPORTED", + } + } + } + + response = self.sourcehut.update_issue( + repo_id="~user/project", + issue_id=123, + title="Updated title", + body="Updated body", + ) + + self.assertEqual(response["id"], 123) + mock_post.assert_called_once() + mock_get_tracker_id.assert_called_once_with("~user/project") + + @mock.patch.object(SourceHutIntegration, "post") + @mock.patch.object(SourceHutIntegration, "get_tracker_id", return_value=1) + def test_sourcehut_post_comment_calls_post(self, mock_get_tracker_id, mock_post): + mock_post.return_value = { + "data": { + "submitComment": { + "id": 77, + "created": "2025-08-12T12:00:00Z", + "ticket": {"id": 123, "subject": "Example Request"}, + } + } + } + + response = self.sourcehut.post_comment( + repo_id="~user/project", + issue_id=123, + comment_body="Test comment", + ) + + self.assertEqual(response["id"], 77) + mock_post.assert_called_once() + mock_get_tracker_id.assert_called_once_with("~user/project") diff --git a/workflow/tests/test_models.py b/workflow/tests/test_models.py index d618d205..07231e4d 100644 --- a/workflow/tests/test_models.py +++ b/workflow/tests/test_models.py @@ -7,6 +7,7 @@ # import json +from unittest import mock from django.contrib.contenttypes.models import ContentType from django.test import TestCase @@ -350,6 +351,30 @@ def test_request_queryset_status_methods(self): self.assertIn(self.request2, qs) self.assertNotIn(request3, qs) + def test_request_model_close_method(self): + self.request1.close(user=self.super_user, reason="Reason") + self.request1.refresh_from_db() + self.assertEqual(Request.Status.CLOSED, self.request1.status) + self.assertEqual(self.super_user, self.request1.last_modified_by) + close_event = self.request1.events.get(event_type=RequestEvent.CLOSED) + self.assertEqual(self.super_user, close_event.user) + self.assertEqual("Reason", close_event.text) + + def test_request_model_link_external_issue(self): + external_issue = self.request1.link_external_issue( + platform="github", + repo="org/repo", + issue_id="1", + ) + self.assertEqual(self.nexb_dataspace, external_issue.dataspace) + self.assertEqual("github", external_issue.platform) + self.assertEqual("org/repo", external_issue.repo) + self.assertEqual("1", external_issue.issue_id) + self.assertEqual("https://github.com/org/repo/issues/1", external_issue.issue_url) + + self.request1.refresh_from_db() + self.assertEqual(external_issue, self.request1.external_issue) + def test_request_model_is_draft_property(self): self.request1.status = Request.Status.OPEN self.assertFalse(self.request1.is_draft) @@ -564,3 +589,21 @@ def test_request_content_object_update_request_count_on_delete(self): component1.delete() request1.delete() + + @mock.patch("workflow.integrations.github.GitHubIntegration.sync") + def test_sync_called_on_save_when_issue_tracker_is_github(self, mock_sync): + mock_sync.return_value = None + self.nexb_dataspace.set_configuration("github_token", "fake-token") + + self.request_template1.create_request( + title="Example Request", + requester=self.super_user, + ) + mock_sync.assert_not_called() + + self.request_template1.update(issue_tracker_id="https://github.com/org/repo") + request = self.request_template1.create_request( + title="Example Request", + requester=self.super_user, + ) + mock_sync.assert_called_once_with(request=request) diff --git a/workflow/views.py b/workflow/views.py index d38fcdf1..ecb13e8d 100644 --- a/workflow/views.py +++ b/workflow/views.py @@ -18,7 +18,7 @@ from django.shortcuts import get_object_or_404 from django.shortcuts import redirect from django.shortcuts import render -from django.utils.html import format_html +from django.utils.html import mark_safe from dje.utils import get_preserved_filters from dje.utils import group_by @@ -45,7 +45,6 @@ class RequestListView( filterset_class = RequestFilterSet template_name = "workflow/request_list.html" template_list_table = "workflow/includes/request_list_table.html" - paginate_by = 50 def get_queryset(self): """ @@ -123,7 +122,7 @@ def request_add_view(request, template_uuid): f"" ) - messages.success(request, format_html(msg)) + messages.success(request, mark_safe(msg)) return redirect(instance.get_absolute_url()) return render(request, "workflow/request_form.html", {"form": form}) @@ -187,7 +186,7 @@ def request_edit_view(request, request_uuid): f"" ) - messages.success(request, format_html(msg)) + messages.success(request, mark_safe(msg)) return redirect(request_instance) elif not form.has_changed(): @@ -244,15 +243,7 @@ def request_details_view(request, request_uuid): closed_reason = request.POST.get("closed_reason") if closed_reason and request_instance.has_close_permission(request.user): - request_instance.status = Request.Status.CLOSED - request_instance.last_modified_by = request.user - request_instance.save() - event_instance = request_instance.events.create( - user=request.user, - text=closed_reason, - event_type=RequestEvent.CLOSED, - dataspace=request_instance.dataspace, - ) + event_instance = request_instance.close(user=request.user, reason=closed_reason) send_request_comment_notification(request, event_instance, closed=True) messages.success(request, f"Request {request_instance} closed") return redirect("workflow:request_list")