Dependency licenses¶
pyDEXPI checks the licenses of its dependencies automatically. Every pull request runs the check, so a dependency with an incompatible license is caught in review rather than after release.
This page is for maintainers: it explains the policy, how to run the check locally, and what to do when it fails.
Why the policy is permissive-only¶
pyDEXPI is released under the AGPL-3.0, but is also offered under a commercial license on request. That second option is what drives the policy.
If pyDEXPI depended on a copyleft library, it could no longer be relicensed for a customer who needs a proprietary build — the dependency's own terms would follow it. So the policy is stricter than "compatible with the AGPL": only permissive licenses are allowed by default.
Allowed without review:
| Family | Examples |
|---|---|
| MIT | MIT, MIT-CMU |
| BSD | 0BSD, BSD-2-Clause, BSD-3-Clause |
| Apache | Apache-2.0 |
| ISC | ISC |
| Python | Python-2.0, PSF-2.0 |
| Public domain | CC0-1.0, Unlicense |
| Other permissive | BSL-1.0 (Boost), NCSA, UPL |
Not allowed without a reviewed exception: GPL, LGPL, AGPL, MPL, EPL, EUPL, and anything proprietary. Also not allowed: packages whose license metadata is missing or unreadable. An unknown license is not evidence of a permissive one, so it fails until somebody checks it.
Failing the policy is not the same as being legally incompatible
The policy is deliberately stricter than compatibility. Weak-copyleft licenses such as MPL-2.0 and the LGPL permit a larger work to be distributed under other terms, keeping obligations on the covered files or library themselves — they do not normally prevent commercial or proprietary use. They fail this policy because pyDEXPI's default is permissive-only, not because the dependency is problematic.
The report and license-exceptions.toml therefore distinguish a reviewed weak-copyleft
dependency from a genuine policy exception. Nothing in this repository is legal advice
or legal approval; the exceptions are the maintainers' compatibility assessments.
Where the policy lives¶
| File | Contents |
|---|---|
pyproject.toml, [tool.licensecheck] |
The policy: allowed licenses, disallowed licenses, which extras to scan. |
license-exceptions.toml |
Packages exempted after a manual review, with evidence. |
scripts/licenses |
The runner, used identically in CI and locally. |
scripts/license_report.py |
Builds the Markdown report and validates the exceptions file. |
.github/workflows/licenses.yml |
Runs the check on every push to main and every pull request. |
The check covers the core dependencies and every extra (png, pdf, export, cli, dev),
plus everything those pull in transitively — currently around 70 packages.
Dependency groups and what they mean¶
The whole tree is checked, but the report records why each package is present, because that
changes how seriously a finding should be taken. Groups are resolved with uv, one per extra, and
each package is attributed to the first group below that contains it:
| Group | Reaches users? |
|---|---|
core (runtime) |
Yes — installed by every pip install pyDEXPI |
cli extra, png extra, pdf extra, mcp extra |
Yes — installed by users who opt in, and redistributable by them |
docs tooling, test tooling, lint tooling |
No — only used to build and test pyDEXPI |
A copyleft library inside the pdf extra is a real question: users install it and may
redistribute it. The same license in the docs toolchain is close to irrelevant, because it never
leaves our CI. The report splits failures along exactly that line, so review effort goes where it
matters.
This is why the dev extra is also declared as three narrower extras (lint, test, docs).
dev remains their union, so pip install .[dev] is unchanged — the split exists so the report
can tell documentation tooling apart from test tooling.
Running the check locally¶
uv is required, not optional. licensecheck shells out to it to resolve the dependency tree
from pyproject.toml; its other resolvers cannot read a pyproject.toml and fall back to
inspecting only already-installed packages, one level deep. That produces a report that looks
clean because it is incomplete, so scripts/licenses refuses to run without uv.
The run writes license-report.json (machine-readable, the full inventory) and
license-report.md (the summary). Both are gitignored, and CI uploads them as the
dependency-license-report artifact — including when the job fails, which is when you most want
them. In CI the summary is also written to the workflow's Summary tab, so the failing package
is visible without downloading anything.
When the check fails¶
The report names the offending package, its license, and whether it is direct (declared in
our own pyproject.toml) or transitive (pulled in by something else). Start there:
- Direct dependency. We chose it. Prefer a permissively licensed alternative. If there is no reasonable alternative, weigh the license against the commercial-licensing constraint above before considering an exception.
- Transitive dependency. Check which of our dependencies pulls it in (
uv pip treeorpipdeptree). Sometimes an upstream bump removes it, or the parent offers an extra that avoids it. - Unknown or missing metadata. Usually a packaging bug upstream, not a licensing problem.
Read the project's actual
LICENSEfile and record what you find.
Never silence a failure by loosening the allowlist for one package — widening
only_licenses allows that license for every dependency, now and in future. Use an exception,
which is scoped to the one package and carries a reason.
Do not add ignore_licenses to the policy either. Terms licensecheck does not recognise
collapse into its UNKNOWN category, so ignoring one unrecognised license would quietly mark
every package with unreadable metadata as compliant.
Adding a reviewed exception¶
- Verify the license at the source. Read the
LICENSEfile in the project's own repository, not just the PyPI classifier — classifiers are frequently wrong or stale. - Decide whether it is genuinely acceptable, and write down why. Useful questions: is it an optional extra or a core dependency? Is it shipped to users or only used to build and test? Do we modify it? Would a weak-copyleft library's terms actually reach our code?
- Add an entry to
license-exceptions.toml:
[[exception]]
package = "svglib"
version_spec = ">=2.0.0,<3.0.0"
verified_license = "LGPL-3.0-or-later"
classification = "weak-copyleft"
source = "https://pypi.org/project/svglib/ (License-Expression metadata field)"
reviewed_on = "2026-07-30"
reviewed_by = "pyDEXPI maintainers"
reason = """
Why this is acceptable for pyDEXPI.
"""
package, version_spec, verified_license, classification, source, reviewed_on and
reason are all required; the check fails if any is missing. When the report flags an unknown
license it prints a ready-made template you can paste.
classification must be either weak-copyleft (file- or library-level copyleft such as MPL
or LGPL — reviewed and found not to conflict) or policy-exception (something the policy
genuinely disallows, accepted anyway for the stated reason). The second should be rare and
should attract close scrutiny.
- Get it reviewed. The exception is a licensing decision, so it should be approved by a maintainer in the pull request like any other change.
version_spec is what keeps an exception honest. The check fails if the version CI resolves
falls outside the range that was actually reviewed, so a major upgrade forces a fresh look
rather than inheriting a verdict from an older release. Keep ranges tight enough that a
relicensing upstream cannot slip through — a major-version bound is usually the right call.
If a package no longer needs its exception — it was dropped, or upstream relicensed — the report flags the entry as stale so it can be deleted. That is a warning rather than a failure, so a routine dependency bump does not break CI.
Current exceptions¶
Three, all classified as reviewed weak-copyleft dependencies rather than policy exceptions, all
reviewed on 2026-07-30, with the full reasoning in license-exceptions.toml:
| Package | License | Group | Assessment |
|---|---|---|---|
svglib |
LGPL-3.0-or-later | pdf extra — reaches users |
Unmodified, separately identifiable dependency. LGPL-3.0 permits distributing a Combined Work under other terms, subject to its requirements for the library itself and the user's ability to replace or modify it. |
certifi |
MPL-2.0 | mcp extra — reaches users |
Not an incompatibility. File-level copyleft, a separate package used unmodified; it arrives through httpx (and, for the docs build, requests). |
pathspec |
MPL-2.0 | docs tooling | Same as certifi, via mkdocs. |
svglib and certifi are the two of the three that reach users. certifi needs no more than
the table says: MPL-2.0 binds only its own files, which pyDEXPI never modifies. svglib is the
one worth re-reading. Its
assessment holds for pyDEXPI as distributed today under the AGPL-3.0. The case that would change
the answer is bundling it into a proprietary or frozen executable, where preserving notices,
source availability and the user's replacement rights becomes the binding constraint — re-assess
before doing that, and re-assess if svglib is ever vendored, patched, or promoted to a core
dependency.
Updating the pinned tools¶
.github/workflows/licenses.yml pins licensecheck and uv on purpose: a checker that changes
its verdicts between runs is worse than no checker. When bumping either pin, run
./scripts/licenses locally first and check that the package count and the set of exceptions
have not changed unexpectedly.