Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
c6d2b62
chore(deps): bump dev dependencies and declare django 6.1 support
oliverhaas Aug 14, 2026
2e7ebb8
fix(locmem): correct state sharing, culling, and collection edge cases
oliverhaas Aug 14, 2026
fa5fbd8
fix(semaphore): close acquire and release races and reap dead waiters
oliverhaas Aug 14, 2026
1b68f61
fix(stream): publish broadcasts under the local write lock
oliverhaas Aug 14, 2026
5988afc
fix(tiered): support stock django l2 backends and reject key_prefix
oliverhaas Aug 14, 2026
d7037c2
fix(database): correct compound op writes, like escaping, and numeric…
oliverhaas Aug 14, 2026
9b4bd0c
fix(pipeline): clear decoders when execute fails
oliverhaas Aug 14, 2026
bcca820
fix(glide): apply connection options and complete the command surface
oliverhaas Aug 14, 2026
816a459
fix(redis-rs): honor timeout=0 in set with nx, xx, and get
oliverhaas Aug 14, 2026
1612abd
fix(valkey-py): build clients from the full url and fix pool reuse
oliverhaas Aug 14, 2026
66e83ec
fix(resp): correct encoding, decompression, key reversal, and touch ttl
oliverhaas Aug 14, 2026
84dbc9e
fix(admin): preserve ttl and persistence when editing a key
oliverhaas Aug 14, 2026
151bdef
fix(admin): quote cache and key names in urls and primary keys
oliverhaas Aug 14, 2026
56d5815
ci: wait for every workflow run before dependabot automerge
oliverhaas Aug 14, 2026
03124e6
ci: test against django 6.1 and add a valkey-glide adapter job
oliverhaas Aug 14, 2026
890b111
docs: mark the rust and valkey-glide adapters as experimental
oliverhaas Aug 14, 2026
c0208df
docs: correct backend, parser, serializer, and compression documentation
oliverhaas Aug 14, 2026
0c06dcd
docs: record the audit fixes in the changelog
oliverhaas Aug 14, 2026
64ea02e
docs: exclude internal planning notes from the site build
oliverhaas Aug 14, 2026
e17b0ba
style: reformat python examples in the docs
oliverhaas Aug 14, 2026
031c1ed
style: remove em-dashes from comments, docstrings, and readmes
oliverhaas Aug 14, 2026
103cc22
refactor: inline single-use slowlog and sentinel-key helpers
oliverhaas Aug 14, 2026
27385f6
style: match the repo's section comment style in glide adapter tests
oliverhaas Aug 14, 2026
7eb49fd
test: drop two comments that restate the assertion below them
oliverhaas Aug 14, 2026
805745d
fix(stream): honor KEY_FUNCTION in keys, reverse_key, and make_pattern
oliverhaas Aug 15, 2026
807ef3d
refactor(pipeline): drop unreachable AsyncPipeline.__exit__
oliverhaas Aug 15, 2026
52e2ad4
fix(admin): probe backend capabilities instead of catching AttributeE…
oliverhaas Aug 15, 2026
e986da3
docs: document REVERSE_KEY_FUNCTION and the touch stampede override
oliverhaas Aug 15, 2026
8842e3d
docs: record the remaining audit fixes in the changelog
oliverhaas Aug 15, 2026
f2ae021
build: lint admin templates with djlint and bump pre-commit hooks
oliverhaas Aug 15, 2026
2f93507
ci: run the full backend suite against valkey-glide
oliverhaas Aug 15, 2026
cf2cb0d
ci: drop the removed cibuildwheel cpython-freethreading enable group
oliverhaas Aug 16, 2026
d31e331
fix(glide): translate WRONGTYPE responses into WrongTypeError
oliverhaas Aug 16, 2026
253ef45
fix(glide): drive cluster SCAN through ClusterScanCursor
oliverhaas Aug 16, 2026
384ab60
style(glide): trim the WRONGTYPE section comment
oliverhaas Aug 16, 2026
228cca3
style(glide): trim the cluster scan comment
oliverhaas Aug 16, 2026
fc0e573
style(glide): move the WRONGTYPE rationale into its docstring
oliverhaas Aug 16, 2026
343dba2
fix(admin): redirect instead of raising for unconfigured cache in key…
oliverhaas Aug 16, 2026
9adfe0b
style: trim scan comment in glide cluster adapter
oliverhaas Aug 16, 2026
020fb1b
docs: correct api signatures, cluster lock scope, and wheel platforms
oliverhaas Aug 16, 2026
0215561
fix(glide): support the context manager protocol on the wrongtype proxy
oliverhaas Aug 16, 2026
ddbe259
fix(lock): reject sync with on AsyncLock instead of running unlocked
oliverhaas Aug 16, 2026
70d7867
fix(redis-rs): raise NotSupportedError for the unimplemented slowlog …
oliverhaas Aug 16, 2026
fb6dc3b
docs: correct remaining sorted-set, stream, and set_many parameter names
oliverhaas Aug 16, 2026
94d5756
merge: origin/main into fix/production-readiness-audit
oliverhaas Aug 16, 2026
f1f6606
test: tolerate the second Redis TTL rounding can shave off
oliverhaas Aug 16, 2026
243413a
test: hold the semaphore slot until the queue-jumper has tried
oliverhaas Aug 17, 2026
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
50 changes: 44 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,14 @@ jobs:
- "3.14"
django-version:
- "6.0"
- "6.1"
client-library-version:
- "latest"

include:
# Pre-release client libraries (redis-py master, valkey-py main)
- python-version: "3.14"
django-version: "6.0"
django-version: "6.1"
client-library-version: "dev"

# Pre-release Django
Expand Down Expand Up @@ -84,6 +85,10 @@ jobs:

- name: Run backend tests
run: uv run pytest tests/cache/ -n auto
env:
# Guard against pyc write races on first concurrent import
# (intermittent xdist collection ImportError on fresh checkouts).
PYTHONDONTWRITEBYTECODE: "1"

- name: Upload coverage to Codecov
if: matrix.python-version == '3.14' && matrix.django-version == '6.0' && matrix.client-library-version == 'latest'
Expand All @@ -94,6 +99,10 @@ jobs:

test-admin:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
django-version: ["6.0", "6.1"]
steps:
- uses: actions/checkout@v7

Expand All @@ -106,11 +115,37 @@ jobs:
- name: Install dependencies
run: uv sync --group dev

- name: Install Django
run: uv pip install "Django~=6.0.0"
- name: Install Django ${{ matrix.django-version }}
run: uv pip install "Django~=${{ matrix.django-version }}.0"

- name: Run admin tests
run: uv run pytest tests/admin/ -n auto
env:
PYTHONDONTWRITEBYTECODE: "1"

test-glide-adapter:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7

- name: Install uv
uses: astral-sh/setup-uv@v7

# glide ships no free-threaded wheel, so this runs on GIL cp314 only.
- name: Set up Python
run: uv python install 3.14

- name: Install dependencies
run: uv sync --group dev --extra valkey-glide

# The cache fixtures skip an adapter whose client library is missing
# (_adapter_library_available in tests/fixtures/cache.py), and no other
# job installs the glide extra, so this is the only place the shared
# suite reaches the glide adapter.
- name: Run backend tests with the glide adapter
run: uv run pytest tests/cache/ -n auto
env:
PYTHONDONTWRITEBYTECODE: "1"

build-pure-wheel:
name: Build pure-Python wheel (django-cachex)
Expand Down Expand Up @@ -163,10 +198,11 @@ jobs:
with:
package-dir: crates/django-cachex-redis-rs
env:
# No CIBW_ENABLE: cibuildwheel 4.0 dropped the cpython-freethreading
# group and builds 3.14t whenever CIBW_BUILD asks for it.
CIBW_BUILD: ${{ matrix.cibw_build }}
CIBW_ENABLE: cpython-freethreading
CIBW_ARCHS: ${{ matrix.cibw_archs }}
# Linux builds run inside manylinux containers install Rust there.
# Linux builds run inside manylinux containers, so install Rust there.
# macOS and Windows runners have rustup preinstalled.
CIBW_BEFORE_ALL_LINUX: |
curl --proto =https --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal
Expand Down Expand Up @@ -210,6 +246,8 @@ jobs:

- name: Run cache tests against built wheels
run: uv run --no-sync pytest tests/cache/ -n auto
env:
PYTHONDONTWRITEBYTECODE: "1"

smoke-test-wheel-freethreaded:
name: Smoke-test cp314t rust wheel
Expand Down Expand Up @@ -239,7 +277,7 @@ jobs:
uv pip install --no-deps wheelhouse/django_cachex_redis_rs-*-cp314-cp314t-*.whl

- name: Verify FT wheel imports
run: uv run --no-sync python -c "import sys; from django_cachex.adapters import _redis_rs; assert not sys._is_gil_enabled(), 'GIL is enabled not running on free-threaded build'; print('OK', _redis_rs, sys.version)"
run: uv run --no-sync python -c "import sys; from django_cachex.adapters import _redis_rs; assert not sys._is_gil_enabled(), 'GIL is enabled, not running on free-threaded build'; print('OK', _redis_rs, sys.version)"

smoke-test-pure-only:
name: Smoke-test opt-out (pure wheel only, no rust package)
Expand Down
36 changes: 34 additions & 2 deletions .github/workflows/dependabot-automerge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,41 @@ jobs:
automerge:
runs-on: ubuntu-latest
if: github.actor == 'dependabot[bot]'
timeout-minutes: 30
steps:
- name: Enable auto-merge
run: gh pr merge --auto --squash "$PR_URL"
# Without required status checks on main, `gh pr merge --auto` merges
# immediately, red CI included. Wait for every workflow run on this PR
# head instead, and merge only once they have all succeeded.
#
# Poll check suites, not check runs: a `needs:`-gated job gets no check
# run until its dependencies finish, so a poll landing in that gap sees
# nothing pending and merges before the wheel tests have even started.
# A suite stays in progress across the gap. This job's own suite is
# excluded so it does not wait on itself forever.
- name: Wait for CI
run: |
sha=$(gh pr view "$PR_URL" --json headRefOid --jq .headRefOid)
own=$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" --jq .check_suite_id)
while true; do
suites=$(gh api "repos/$GITHUB_REPOSITORY/commits/$sha/check-suites?per_page=100" \
--jq "[.check_suites[] | select(.id != $own) | {status, conclusion}]" || true)
[ -n "$suites" ] || suites='[]'
bad=$(jq -c '[.[] | select(.conclusion != null and ([.conclusion] | inside(["success", "neutral", "skipped"]) | not))]' <<<"$suites")
if [ "$(jq 'length' <<<"$bad")" -gt 0 ]; then
echo "CI did not pass: $bad"
exit 1
fi
if [ "$(jq 'length' <<<"$suites")" -gt 0 ] && [ "$(jq '[.[] | select(.status != "completed")] | length' <<<"$suites")" -eq 0 ]; then
break
fi
sleep 30
done
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Merge
run: gh pr merge --squash "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
5 changes: 3 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,15 @@ jobs:
with:
package-dir: crates/django-cachex-redis-rs
env:
# No CIBW_ENABLE: cibuildwheel 4.0 dropped the cpython-freethreading
# group and builds 3.14t whenever CIBW_BUILD asks for it.
CIBW_BUILD: ${{ matrix.cibw_build }}
CIBW_ENABLE: cpython-freethreading
CIBW_ARCHS: ${{ matrix.cibw_archs }}
CIBW_BEFORE_ALL_LINUX: |
curl --proto =https --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal
CIBW_ENVIRONMENT_LINUX: PATH="$HOME/.cargo/bin:$PATH"

# sdist is a single artifact only build it once on the Linux x86_64 leg.
# sdist is a single artifact, so only build it once on the Linux x86_64 leg.
- name: Build Rust sdist
if: matrix.label == 'linux-x86_64'
uses: PyO3/maturin-action@v1
Expand Down
12 changes: 8 additions & 4 deletions .github/workflows/tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:

if curl -s "https://pypi.org/pypi/django-cachex/$CURRENT_VERSION/json" | grep -q '"version"'; then
echo "should_release=false" >> $GITHUB_OUTPUT
echo "Version $CURRENT_VERSION already on PyPI nothing to do"
echo "Version $CURRENT_VERSION already on PyPI, nothing to do"
exit 0
fi

Expand All @@ -33,7 +33,7 @@ jobs:
git fetch --tags
if git rev-parse "v$CURRENT_VERSION" >/dev/null 2>&1; then
echo "should_release=false" >> $GITHUB_OUTPUT
echo "Tag v$CURRENT_VERSION already exists delete it manually to re-release"
echo "Tag v$CURRENT_VERSION already exists; delete it manually to re-release"
exit 0
fi

Expand Down Expand Up @@ -70,6 +70,10 @@ jobs:

- name: Run tests
run: uv run pytest tests/cache/ -n auto
env:
# Guard against pyc write races on first concurrent import
# (intermittent xdist collection ImportError on fresh checkouts).
PYTHONDONTWRITEBYTECODE: "1"

tag-and-trigger:
needs: [check-version, test]
Expand All @@ -90,8 +94,8 @@ jobs:
git tag "v$VERSION"
git push origin "v$VERSION"

# GITHUB_TOKEN-pushed tags don't trigger downstream workflows — explicit
# dispatch is required.
# GITHUB_TOKEN-pushed tags don't trigger downstream workflows, so an
# explicit dispatch is required.
- name: Trigger publish workflow
run: gh workflow run publish.yml -f version=$VERSION -R ${{ github.repository }}
env:
Expand Down
11 changes: 10 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ default_language_version:

repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
rev: v6.0.0
hooks:
- id: check-ast
- id: check-case-conflict
Expand Down Expand Up @@ -54,6 +54,15 @@ repos:
entry: uv run ruff format
language: system
pass_filenames: false
# `-` makes djLint take its file list from `files` in [tool.djlint]; it
# ignores anything pre-commit would append, so don't pass filenames.
# PYTHON_GIL=1 because djLint's compiled module initialises unsafely on
# the free-threaded build and dies with a NameError on _LINE_PATTERN.
- id: djlint
name: djlint
entry: env PYTHON_GIL=1 uv run djlint - --lint
language: system
pass_filenames: false

- repo: local
hooks:
Expand Down
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ CACHES = {
- Cache stampede prevention (TTL-based XFetch).
- Two composite backends: `StreamCache` (cross-pod stream-synchronized in-memory cache) and `TieredCache` (L1/L2 with TTL propagation).
- Django `LocMemCache` and `DatabaseCache` extensions with the same data-structure ops and admin support.
- Optional Rust I/O driver (PyO3 + tokio + redis-rs) under the same `RespCache` API. Free-threaded CPython (3.14t) supported.
- Optional `valkey-glide` adapter: Valkey's official Rust-cored client, exposed as `ValkeyGlideCache`.
- Optional Rust I/O driver (PyO3 + tokio + redis-rs) under the same `RespCache` API. Free-threaded CPython (3.14t) supported. Experimental.
- Optional `valkey-glide` adapter: Valkey's official Rust-cored client, exposed as `ValkeyGlideCache`. Experimental.
- Django admin UI for browsing keys, inspecting values, editing, and flushing. See below.

## Cache Admin
Expand Down Expand Up @@ -78,15 +78,18 @@ Full documentation at [oliverhaas.github.io/django-cachex](https://oliverhaas.gi
- Valkey 7.0+ or Redis 6.0+ on the server (the admin's compare-and-swap
edits use `SET ... KEEPTTL`, which lands in Redis 6.0)

The Rust I/O driver is optional. To opt in, install with the `redis-rs`
The Rust I/O driver is optional and experimental: interfaces and
behavior may still change, and it has seen less production testing than
the redis-py/valkey-py paths. To opt in, install with the `redis-rs`
extra (`pip install django-cachex[redis-rs]`); this pulls in the
`django-cachex-redis-rs` companion package. Prebuilt wheels are published
for Linux x86_64, Linux aarch64, macOS arm64, and Windows amd64, on
both cp314 and cp314t (free-threaded). Without the extra, the
`RedisRsCache` backends are unavailable but everything else works.

The `valkey-glide` adapter is also optional. Install with the
`valkey-glide` extra (`pip install django-cachex[valkey-glide]`) to enable
The `valkey-glide` adapter is also optional and experimental, with the
same caveats. Install with the `valkey-glide` extra
(`pip install django-cachex[valkey-glide]`) to enable
`ValkeyGlideCache`; it pulls in `valkey-glide-sync` and `valkey-glide`,
the official Rust-cored Valkey client. cp314 GIL only; no free-threaded
wheels yet. Cluster is supported via `ValkeyGlideClusterCache`; Sentinel
Expand Down
Loading
Loading