Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,30 @@ jobs:
files: ./coverage.xml
fail_ci_if_error: false

# Read the Docs builds from its own webhook after merge, so nothing else
# would catch a broken cross-reference or a malformed docstring until the
# docs were already live. This leg runs the same build RTD runs -- same
# -W/nitpicky settings via .readthedocs.yaml's fail_on_warning -- on every
# PR. autodoc imports httpx_pki, so the package is installed, not just the
# docs toolchain.
docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.12"
- name: Install
run: |
python -m pip install --upgrade pip
pip install -e ".[docs]"
- name: Build (warnings are errors)
run: python -m sphinx -b html -W --keep-going docs docs/_build/html
- name: Check external links
# Informational: an upstream site being down should not redden a PR.
continue-on-error: true
run: python -m sphinx -b linkcheck docs docs/_build/linkcheck

# The httpx fallback leg. The main matrix runs on httpx2 (the required
# dependency; the dev extra also installs httpx, exercising the
# both-installed preference). This leg covers the remaining environment the
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ htmlcov/
*.pem
*.key
.idea*
docs/_build/
31 changes: 31 additions & 0 deletions .readthedocs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Read the Docs build configuration.
#
# RTD builds on its own webhook, not from a GitHub Action: a push to main
# rebuilds `latest`, and pushing a vX.Y.Z tag builds that version and moves
# `stable` onto it. Since publish.yml fires on a published release -- which
# implies the tag -- the docs and the PyPI release track the same tag without
# either workflow knowing about the other.
version: 2

build:
os: ubuntu-24.04
tools:
python: "3.12"

sphinx:
configuration: docs/conf.py
# A broken cross-reference or an orphaned page fails the build rather than
# shipping quietly. Paired with nitpicky = True in conf.py.
fail_on_warning: true

python:
install:
# autodoc imports httpx_pki, so the package and its runtime dependencies
# have to be installed, not just the docs toolchain.
- method: pip
path: .
extra_requirements:
- docs

formats:
- htmlzip
75 changes: 75 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,81 @@ the git history for the fine print.
`verify="certifi"` (added in 0.7) pins the certifi bundle for callers who
want the old behavior. `SSLKEYLOGFILE` is honored by every context either
way.
- **Breaking: `from_key_pair`'s `key_password=` is now `password=`**, the same
keyword every other constructor and `reload()` already used. Rename the
argument at call sites: `from_key_pair(cert, key, key_password=...)` becomes
`from_key_pair(cert, key, password=...)`.
- **Breaking: `CertInfo.not_before` / `not_after` are now `not_valid_before` /
`not_valid_after`**, matching the client properties of the same name (and
`cryptography`'s own vocabulary) so the two objects no longer spell the same
instant two ways. `httpx_pki.testing.make_client_cert()` takes the renamed
keywords to match, keeping mint-and-read-back symmetric.
- **Breaking: the platform stores' `predicate=` is now `identity=`**, the same
keyword PKCS#12 and PEM bundles already used, on
`from_windows_cert_store`, `from_macos_keychain`, `build_windows_ssl_context`,
`build_macos_ssl_context`, `select_windows_certificate`, and
`select_macos_certificate`. `identity` is the library's noun everywhere else
(`P12Identity`, `list_identities()`, `HTTPX_PKI_IDENTITY`), and having one
spelling for bundles and another for stores meant `currently_valid` had to be
documented twice in different vocabulary. It now reads
`identity=currently_valid` everywhere.

On the stores `identity=` accepts everything a bundle's does *except* an
integer position: a store has no stable enumeration order, so a position
would select a different certificate from one run to the next, and it raises
`TypeError` rather than silently indexing. A string is a name substring or an
exact SHA-1/SHA-256 fingerprint, matching the bundle rule. `name=` and
`thumbprint=` are unchanged and remain the unambiguous spellings.
- **New: the `_init_state()` subclass hook** — the documented seam for
subclasses that take constructor keywords of their own. It runs exactly once
on every construction path (`__init__`, every `from_*` alternate
constructor, and unpickling — the latter two never call `__init__`, so
extending `__init__` alone was not enough), receiving the extra-keyword dict
before it is forwarded to httpx. Pop your keywords, set your attributes;
what remains must be valid httpx keywords, so unclaimed arguments still fail
loudly. State set in the hook survives a pickle round trip automatically,
and `reload()`/`auto_reload` leave it untouched. See the subclassing section
of the advanced-usage guide.
- **Bug fix: `warn_if_expires_within` now survives `reload()` and pickling.**
The window was applied once at construction and then forgotten, so the
early-expiry warning went permanently quiet after the first rotation — and
after any pickle round trip — which silently disabled the one signal the
documented `auto_reload` + `warn_if_expires_within` pairing exists to give a
long-lived service. It is now retained on the client and re-applied to the
*freshly loaded* certificate on every reload: a rotation onto another
short-lived certificate warns again, one onto a healthy certificate goes
quiet, and a client that never asked for the warning still never gets one.
The two unconditional warnings (expired, not-yet-valid) already fired on
reload and are unchanged.
- **Bug fix: `reload(password=...)` no longer silently discards the password**
for sources that supply their own. A client built by `from_env()` reads
`{prefix}PASSWORD` itself, and the Windows store and macOS keychain export
under an internally generated single-use password — for all three the
argument had nothing to decrypt and was dropped without a word, so removing
a password from the environment and passing it to `reload()` instead failed
with a bare "wrong password" from a caller who had supplied one. It now
raises `TypeError` naming which case you are in and where the password
belongs, matching how `auto_reload` already rejects a source it cannot
watch. Reloads that pass no password are unaffected.
- **Breaking: the certificate-source argument is now `source=` everywhere.**
`PKIClient(...)` / `AsyncPKIClient(...)`, `from_pkcs12`, and
`build_ssl_context` called it `cert=` while `from_pem`, `list_identities`,
and `list_pkcs12_identities` already called it `source=`; the parameter is
typed `CertSource` (a path, `bytes`, or `Path`, and for a bundle it holds a
key and chain as well as a certificate), so `source` describes it and now
names it everywhere. Callers passing it positionally — every example in the
docs — are unaffected.

This also fixes a real defect: because the constructor's first parameter was
named `cert`, httpx's deprecated `cert=` keyword bound to it instead of
reaching the guard, so `PKIClient(bundle, cert=...)` raised a bare
`_PKIMixin.__init__() got multiple values for argument 'cert'` — leaking a
private class name and explaining nothing — where every `from_*` constructor
gave a pointed message. The guard now fires uniformly.

`from_key_pair(certificate=..., private_key=...)` is unchanged: there
`certificate` really is the certificate, distinct from the key. So is
`cert_info(cert_pem)`, which takes PEM bytes rather than a source.
- **truststore is now a direct required dependency** (it also arrives
transitively with httpx2, but httpx-pki calls it directly). The `[system]`
and `[httpx2]` extras still install but are no-ops; they are kept so
Expand Down
Loading