sqlacache 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sqlacache-0.1.0/.github/workflows/ci.yml +26 -0
- sqlacache-0.1.0/.github/workflows/integration.yml +18 -0
- sqlacache-0.1.0/.github/workflows/release.yml +21 -0
- sqlacache-0.1.0/.gitignore +49 -0
- sqlacache-0.1.0/.pre-commit-config.yaml +32 -0
- sqlacache-0.1.0/.prek/README +2 -0
- sqlacache-0.1.0/CHANGELOG.md +13 -0
- sqlacache-0.1.0/CONTRIBUTING.md +49 -0
- sqlacache-0.1.0/LICENSE +21 -0
- sqlacache-0.1.0/Makefile +37 -0
- sqlacache-0.1.0/PKG-INFO +298 -0
- sqlacache-0.1.0/README.md +254 -0
- sqlacache-0.1.0/docker-compose.yml +14 -0
- sqlacache-0.1.0/openspec/changes/implement-mvp/.openspec.yaml +2 -0
- sqlacache-0.1.0/openspec/changes/implement-mvp/design.md +116 -0
- sqlacache-0.1.0/openspec/changes/implement-mvp/proposal.md +42 -0
- sqlacache-0.1.0/openspec/changes/implement-mvp/specs/1-project-infrastructure.md +321 -0
- sqlacache-0.1.0/openspec/changes/implement-mvp/specs/2-cache-configuration.md +212 -0
- sqlacache-0.1.0/openspec/changes/implement-mvp/specs/3-query-interception.md +181 -0
- sqlacache-0.1.0/openspec/changes/implement-mvp/specs/4-cache-key-generation.md +190 -0
- sqlacache-0.1.0/openspec/changes/implement-mvp/specs/5-row-level-invalidation.md +294 -0
- sqlacache-0.1.0/openspec/changes/implement-mvp/specs/6-cashews-transport.md +289 -0
- sqlacache-0.1.0/openspec/changes/implement-mvp/specs/7-cross-process-invalidation.md +315 -0
- sqlacache-0.1.0/openspec/changes/implement-mvp/tasks.md +158 -0
- sqlacache-0.1.0/openspec/config.yaml +20 -0
- sqlacache-0.1.0/pyproject.toml +100 -0
- sqlacache-0.1.0/sqlacache-architecture.md +1277 -0
- sqlacache-0.1.0/src/sqlacache/__init__.py +14 -0
- sqlacache-0.1.0/src/sqlacache/config.py +175 -0
- sqlacache-0.1.0/src/sqlacache/contrib/__init__.py +1 -0
- sqlacache-0.1.0/src/sqlacache/contrib/fastapi.py +1 -0
- sqlacache-0.1.0/src/sqlacache/contrib/prometheus.py +1 -0
- sqlacache-0.1.0/src/sqlacache/exceptions.py +13 -0
- sqlacache-0.1.0/src/sqlacache/interceptor.py +128 -0
- sqlacache-0.1.0/src/sqlacache/invalidation.py +45 -0
- sqlacache-0.1.0/src/sqlacache/manager.py +348 -0
- sqlacache-0.1.0/src/sqlacache/pubsub/__init__.py +3 -0
- sqlacache-0.1.0/src/sqlacache/pubsub/redis.py +89 -0
- sqlacache-0.1.0/src/sqlacache/py.typed +0 -0
- sqlacache-0.1.0/src/sqlacache/serializers/__init__.py +5 -0
- sqlacache-0.1.0/src/sqlacache/serializers/json.py +24 -0
- sqlacache-0.1.0/src/sqlacache/transport/__init__.py +45 -0
- sqlacache-0.1.0/src/sqlacache/transport/cashews.py +136 -0
- sqlacache-0.1.0/src/sqlacache/utils/__init__.py +1 -0
- sqlacache-0.1.0/src/sqlacache/utils/key_generation.py +40 -0
- sqlacache-0.1.0/src/sqlacache/utils/query_analysis.py +108 -0
- sqlacache-0.1.0/src/sqlacache/utils/sync_wrapper.py +16 -0
- sqlacache-0.1.0/tests/__init__.py +1 -0
- sqlacache-0.1.0/tests/conftest.py +102 -0
- sqlacache-0.1.0/tests/integration/__init__.py +1 -0
- sqlacache-0.1.0/tests/integration/conftest.py +50 -0
- sqlacache-0.1.0/tests/integration/test_cross_process_invalidation.py +25 -0
- sqlacache-0.1.0/tests/pubsub/__init__.py +1 -0
- sqlacache-0.1.0/tests/pubsub/test_redis.py +44 -0
- sqlacache-0.1.0/tests/test_config.py +26 -0
- sqlacache-0.1.0/tests/test_interceptor.py +67 -0
- sqlacache-0.1.0/tests/test_invalidation.py +40 -0
- sqlacache-0.1.0/tests/test_key_generation.py +26 -0
- sqlacache-0.1.0/tests/test_manager.py +138 -0
- sqlacache-0.1.0/tests/test_query_analysis.py +35 -0
- sqlacache-0.1.0/tests/transport/__init__.py +1 -0
- sqlacache-0.1.0/tests/transport/conftest.py +38 -0
- sqlacache-0.1.0/tests/transport/test_cashews.py +102 -0
- sqlacache-0.1.0/uv.lock +1694 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
push:
|
|
6
|
+
branches: ["main"]
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
- uses: astral-sh/setup-uv@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: ${{ matrix.python-version }}
|
|
21
|
+
- run: uv sync --extra redis --group dev
|
|
22
|
+
- run: uv run ruff check src/ tests/
|
|
23
|
+
- run: uv run ruff format --check src/ tests/
|
|
24
|
+
- run: uv run mypy src/
|
|
25
|
+
- run: uv run ty src/ || true
|
|
26
|
+
- run: uv run pytest -m "not integration"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
name: Integration
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
schedule:
|
|
6
|
+
- cron: "0 6 * * 1"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
integration:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: astral-sh/setup-uv@v5
|
|
14
|
+
with:
|
|
15
|
+
python-version: "3.12"
|
|
16
|
+
- run: docker compose up -d
|
|
17
|
+
- run: uv sync --extra redis --group dev
|
|
18
|
+
- run: uv run pytest -m integration
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
id-token: write
|
|
13
|
+
contents: read
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- uses: astral-sh/setup-uv@v5
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.10"
|
|
20
|
+
- run: uv build
|
|
21
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
.venv/
|
|
3
|
+
__pycache__/
|
|
4
|
+
*.py[cod]
|
|
5
|
+
*.egg-info/
|
|
6
|
+
dist/
|
|
7
|
+
build/
|
|
8
|
+
|
|
9
|
+
# Type checkers / linters
|
|
10
|
+
.mypy_cache/
|
|
11
|
+
.ruff_cache/
|
|
12
|
+
.ty_cache/
|
|
13
|
+
|
|
14
|
+
# Test / coverage
|
|
15
|
+
.pytest_cache/
|
|
16
|
+
.coverage
|
|
17
|
+
htmlcov/
|
|
18
|
+
|
|
19
|
+
# Package managers
|
|
20
|
+
.uv-cache/
|
|
21
|
+
uv.lock.bak
|
|
22
|
+
|
|
23
|
+
# OS
|
|
24
|
+
.DS_Store
|
|
25
|
+
Thumbs.db
|
|
26
|
+
|
|
27
|
+
# IDE
|
|
28
|
+
.idea/
|
|
29
|
+
.vscode/
|
|
30
|
+
|
|
31
|
+
# AI assistant tool directories (not project source)
|
|
32
|
+
.claude/
|
|
33
|
+
CLAUDE.md
|
|
34
|
+
.codex/
|
|
35
|
+
.gemini/
|
|
36
|
+
.opencode/
|
|
37
|
+
.github/prompts/
|
|
38
|
+
.github/skills/
|
|
39
|
+
|
|
40
|
+
# prek hook runner logs and scratch
|
|
41
|
+
.prek/prek.log
|
|
42
|
+
.prek/scratch/
|
|
43
|
+
|
|
44
|
+
# Scratch / research notes not part of the project
|
|
45
|
+
research.md
|
|
46
|
+
landscape-research.md
|
|
47
|
+
|
|
48
|
+
# Examples (not part of the library itself)
|
|
49
|
+
examples/
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
3
|
+
rev: v5.0.0
|
|
4
|
+
hooks:
|
|
5
|
+
- id: trailing-whitespace
|
|
6
|
+
- id: end-of-file-fixer
|
|
7
|
+
- id: check-yaml
|
|
8
|
+
- id: check-toml
|
|
9
|
+
- id: check-added-large-files
|
|
10
|
+
|
|
11
|
+
- repo: local
|
|
12
|
+
hooks:
|
|
13
|
+
- id: ruff-check
|
|
14
|
+
name: ruff check
|
|
15
|
+
entry: uv run ruff check --fix src/ tests/
|
|
16
|
+
language: system
|
|
17
|
+
pass_filenames: false
|
|
18
|
+
- id: ruff-format
|
|
19
|
+
name: ruff format
|
|
20
|
+
entry: uv run ruff format src/ tests/
|
|
21
|
+
language: system
|
|
22
|
+
pass_filenames: false
|
|
23
|
+
- id: mypy
|
|
24
|
+
name: mypy
|
|
25
|
+
entry: uv run mypy src/
|
|
26
|
+
language: system
|
|
27
|
+
pass_filenames: false
|
|
28
|
+
- id: ty
|
|
29
|
+
name: ty
|
|
30
|
+
entry: uv run ty check src/
|
|
31
|
+
language: system
|
|
32
|
+
pass_filenames: false
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
- Initial alpha MVP release.
|
|
6
|
+
- Added package scaffolding, linting, typing, tests, CI, and build metadata.
|
|
7
|
+
- Added validated configuration and cache manager APIs via `configure(...)`.
|
|
8
|
+
- Added `cashews` transport integration for `mem://` and `redis://` backends.
|
|
9
|
+
- Added automatic async ORM caching for configured reads, including `AsyncSession.get(...)`.
|
|
10
|
+
- Added row-level invalidation for ORM inserts, updates, deletes, and bulk mutations.
|
|
11
|
+
- Added Redis pub/sub support for cross-process invalidation.
|
|
12
|
+
- Added unit tests, Redis integration coverage, and an external wheel-install smoke test workflow.
|
|
13
|
+
- Sync session support remains deferred.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
## Environment
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
uv sync --extra redis --group dev
|
|
7
|
+
prek install
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Common Commands
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
make lint
|
|
14
|
+
make format
|
|
15
|
+
make typecheck
|
|
16
|
+
make test
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Current Scope
|
|
20
|
+
|
|
21
|
+
- The implemented runtime is async SQLAlchemy
|
|
22
|
+
- Redis integration is supported and tested
|
|
23
|
+
- Sync session support is not implemented yet
|
|
24
|
+
- The architecture document includes planned work beyond the current MVP
|
|
25
|
+
|
|
26
|
+
## Release Smoke Test
|
|
27
|
+
|
|
28
|
+
To verify the built package outside the repo, run:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
.venv/bin/python -m build
|
|
32
|
+
.venv/bin/python -m twine check dist/*
|
|
33
|
+
python -m venv /tmp/sqlacache-release-venv
|
|
34
|
+
UV_CACHE_DIR=.uv-cache uv pip install --python /tmp/sqlacache-release-venv/bin/python dist/*.whl
|
|
35
|
+
UV_CACHE_DIR=.uv-cache uv pip install --python /tmp/sqlacache-release-venv/bin/python redis aiosqlite
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Then run a standalone script from `/tmp` that imports `sqlacache` from that external environment and validates:
|
|
39
|
+
|
|
40
|
+
- automatic `session.get(...)` caching
|
|
41
|
+
- Redis keys appearing after cache population
|
|
42
|
+
- invalidation after an update
|
|
43
|
+
- recache with fresh data after invalidation
|
|
44
|
+
|
|
45
|
+
## Workflow
|
|
46
|
+
|
|
47
|
+
- Keep changes focused and aligned with the active OpenSpec change.
|
|
48
|
+
- Run the relevant local checks before opening a pull request.
|
|
49
|
+
- CI should pass before merging.
|
sqlacache-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hamidreza Samsami
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
sqlacache-0.1.0/Makefile
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
.PHONY: help install lint format typecheck test testcov integration clean
|
|
2
|
+
|
|
3
|
+
help:
|
|
4
|
+
@printf "Available targets:\n"
|
|
5
|
+
@printf " install Install project dependencies with uv\n"
|
|
6
|
+
@printf " lint Run ruff checks\n"
|
|
7
|
+
@printf " format Format code with ruff\n"
|
|
8
|
+
@printf " typecheck Run mypy and ty\n"
|
|
9
|
+
@printf " test Run unit tests\n"
|
|
10
|
+
@printf " testcov Run unit tests with coverage\n"
|
|
11
|
+
@printf " integration Run integration tests\n"
|
|
12
|
+
@printf " clean Remove caches, build artifacts, and virtualenv\n"
|
|
13
|
+
|
|
14
|
+
install:
|
|
15
|
+
uv sync --extra redis --group dev
|
|
16
|
+
|
|
17
|
+
lint:
|
|
18
|
+
uv run ruff check src/ tests/
|
|
19
|
+
|
|
20
|
+
format:
|
|
21
|
+
uv run ruff format src/ tests/
|
|
22
|
+
|
|
23
|
+
typecheck:
|
|
24
|
+
uv run mypy src/
|
|
25
|
+
uv run ty check src/ || true
|
|
26
|
+
|
|
27
|
+
test:
|
|
28
|
+
uv run pytest -x -m "not integration"
|
|
29
|
+
|
|
30
|
+
testcov:
|
|
31
|
+
uv run pytest --cov=sqlacache --cov-report=term-missing -m "not integration"
|
|
32
|
+
|
|
33
|
+
integration:
|
|
34
|
+
uv run pytest -m integration
|
|
35
|
+
|
|
36
|
+
clean:
|
|
37
|
+
rm -rf .venv .mypy_cache .pytest_cache .ruff_cache dist build src/*.egg-info .coverage htmlcov
|
sqlacache-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sqlacache
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Django-cacheops-style declarative caching with automatic row-level invalidation for SQLAlchemy
|
|
5
|
+
Project-URL: Homepage, https://github.com/persix/sqlacache
|
|
6
|
+
Project-URL: Repository, https://github.com/persix/sqlacache
|
|
7
|
+
Project-URL: Documentation, https://github.com/persix/sqlacache/blob/main/README.md
|
|
8
|
+
Project-URL: Issues, https://github.com/persix/sqlacache/issues
|
|
9
|
+
Author-email: Hamidreza Samsami <hamidreza.samsami@gmail.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: asyncio,cache,orm,redis,sqlalchemy
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Framework :: AsyncIO
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
25
|
+
Classifier: Topic :: Database
|
|
26
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
27
|
+
Requires-Python: >=3.10
|
|
28
|
+
Requires-Dist: cashews<8.0,>=7.0
|
|
29
|
+
Requires-Dist: sqlalchemy>=1.4
|
|
30
|
+
Provides-Extra: all
|
|
31
|
+
Requires-Dist: asyncpg>=0.30.0; extra == 'all'
|
|
32
|
+
Requires-Dist: cashews[dill,diskcache,redis,speedup]<8.0,>=7.0; extra == 'all'
|
|
33
|
+
Provides-Extra: dill
|
|
34
|
+
Requires-Dist: cashews[dill]<8.0,>=7.0; extra == 'dill'
|
|
35
|
+
Provides-Extra: diskcache
|
|
36
|
+
Requires-Dist: cashews[diskcache]<8.0,>=7.0; extra == 'diskcache'
|
|
37
|
+
Provides-Extra: postgresql
|
|
38
|
+
Requires-Dist: asyncpg>=0.30.0; extra == 'postgresql'
|
|
39
|
+
Provides-Extra: redis
|
|
40
|
+
Requires-Dist: cashews[redis]<8.0,>=7.0; extra == 'redis'
|
|
41
|
+
Provides-Extra: speedup
|
|
42
|
+
Requires-Dist: cashews[speedup]<8.0,>=7.0; extra == 'speedup'
|
|
43
|
+
Description-Content-Type: text/markdown
|
|
44
|
+
|
|
45
|
+
# sqlacache
|
|
46
|
+
|
|
47
|
+
A slick library that adds automatic queryset caching and row-level invalidation to SQLAlchemy async sessions.
|
|
48
|
+
|
|
49
|
+
[](https://github.com/hr-samsami/sqlacache/actions/workflows/ci.yml)
|
|
50
|
+
[](https://pypi.org/project/sqlacache/)
|
|
51
|
+
[](https://pypi.org/project/sqlacache/)
|
|
52
|
+
[](LICENSE)
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
`session.get(User, 42)` is cached. `session.commit()` invalidates it. That's the whole idea.
|
|
57
|
+
|
|
58
|
+
Built on top of [`cashews`](https://github.com/Krukov/cashews) for storage and tag-based dependency tracking.
|
|
59
|
+
|
|
60
|
+
- Zero changes to your session usage — reads are intercepted automatically
|
|
61
|
+
- Row-level invalidation — only the rows that changed are evicted, not the whole table
|
|
62
|
+
- Cross-process invalidation via Redis pub/sub — all workers stay in sync
|
|
63
|
+
- Declarative config — map models to ops and TTLs in one place, with wildcard fallback
|
|
64
|
+
- Two backends: `redis://` for production, `mem://` for dev and testing
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Requirements
|
|
69
|
+
|
|
70
|
+
- Python 3.10+
|
|
71
|
+
- SQLAlchemy >= 1.4
|
|
72
|
+
- cashews >= 7.0
|
|
73
|
+
- Redis (for production; not needed for `mem://`)
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Installation
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
pip install sqlacache
|
|
81
|
+
pip install "sqlacache[redis]" # with Redis support
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Using `uv`:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
uv add "sqlacache[redis]"
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Setup
|
|
93
|
+
|
|
94
|
+
Call `configure()` once at startup — alongside your engine setup.
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from sqlalchemy.ext.asyncio import create_async_engine
|
|
98
|
+
from sqlacache import configure
|
|
99
|
+
|
|
100
|
+
engine = create_async_engine("postgresql+asyncpg://user:pass@localhost/mydb")
|
|
101
|
+
|
|
102
|
+
sqlacache = configure(
|
|
103
|
+
backend="redis://localhost:6379/1",
|
|
104
|
+
models={
|
|
105
|
+
"app.models.User": {"ops": {"get", "fetch"}, "timeout": 900},
|
|
106
|
+
"app.models.Product": {"ops": "all", "timeout": 3600},
|
|
107
|
+
"*": {"timeout": 3600},
|
|
108
|
+
},
|
|
109
|
+
)
|
|
110
|
+
await sqlacache.bind(engine)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
That's all the wiring needed. From here, your sessions work as normal.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Usage
|
|
118
|
+
|
|
119
|
+
### Reads are cached automatically
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
async with AsyncSession(engine) as session:
|
|
123
|
+
user = await session.get(User, 42) # cache miss → fetches from DB, stores result
|
|
124
|
+
user = await session.get(User, 42) # cache hit → returned instantly
|
|
125
|
+
|
|
126
|
+
result = await session.execute(select(User).where(User.active == True))
|
|
127
|
+
users = result.scalars().all() # multi-row fetch, also cached
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Writes invalidate the cache automatically
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
async with AsyncSession(engine) as session:
|
|
134
|
+
user = await session.get(User, 42)
|
|
135
|
+
user.name = "Alice"
|
|
136
|
+
await session.commit() # evicts all cached reads that touched User id=42
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
No decorators. No manual cache keys. No changes to how you write queries.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## FastAPI Example
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from contextlib import asynccontextmanager
|
|
147
|
+
from fastapi import FastAPI, Depends
|
|
148
|
+
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
|
|
149
|
+
from sqlacache import configure
|
|
150
|
+
|
|
151
|
+
engine = create_async_engine("postgresql+asyncpg://user:pass@localhost/mydb")
|
|
152
|
+
session_maker = async_sessionmaker(engine, expire_on_commit=False)
|
|
153
|
+
|
|
154
|
+
sqlacache = configure(
|
|
155
|
+
backend="redis://localhost:6379/1",
|
|
156
|
+
models={"app.models.User": {"ops": "all", "timeout": 300}},
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@asynccontextmanager
|
|
161
|
+
async def lifespan(app: FastAPI):
|
|
162
|
+
await sqlacache.bind(engine)
|
|
163
|
+
yield
|
|
164
|
+
await sqlacache.disconnect()
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
app = FastAPI(lifespan=lifespan)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
async def get_session():
|
|
171
|
+
async with session_maker() as session:
|
|
172
|
+
yield session
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@app.get("/users/{user_id}")
|
|
176
|
+
async def get_user(user_id: int, session: AsyncSession = Depends(get_session)):
|
|
177
|
+
return await session.get(User, user_id) # cached after the first request
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Configuration Reference
|
|
183
|
+
|
|
184
|
+
### `configure()`
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
sqlacache = configure(
|
|
188
|
+
backend="redis://localhost:6379/1", # or "mem://" for in-memory
|
|
189
|
+
models={...},
|
|
190
|
+
prefix="sqlacache", # key prefix (default: "sqlacache")
|
|
191
|
+
default_timeout=3600, # TTL when not specified per model (default: 3600)
|
|
192
|
+
serializer="sqlalchemy", # cashews serializer (default: "sqlalchemy")
|
|
193
|
+
)
|
|
194
|
+
await sqlacache.bind(engine)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Model config
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
models={
|
|
201
|
+
"app.models.User": {
|
|
202
|
+
"ops": {"get", "fetch"}, # operations to cache
|
|
203
|
+
"timeout": 900, # TTL in seconds
|
|
204
|
+
},
|
|
205
|
+
"app.models.Product": {"ops": "all", "timeout": 3600},
|
|
206
|
+
"*": {"timeout": 3600}, # wildcard fallback
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
**Ops:**
|
|
211
|
+
|
|
212
|
+
| Op | What it covers |
|
|
213
|
+
| --- | --- |
|
|
214
|
+
| `"get"` | `session.get(Model, pk)` |
|
|
215
|
+
| `"fetch"` | `session.execute(select(Model))` |
|
|
216
|
+
| `"count"` | `select(func.count())` queries |
|
|
217
|
+
| `"exists"` | `select(exists(...))` queries |
|
|
218
|
+
| `"all"` | All four above |
|
|
219
|
+
|
|
220
|
+
### Backends
|
|
221
|
+
|
|
222
|
+
| Backend | URL | Notes |
|
|
223
|
+
| --- | --- | --- |
|
|
224
|
+
| Redis | `redis://host:port/db` | Production; enables cross-process invalidation |
|
|
225
|
+
| In-memory | `mem://` | Dev and testing; no infrastructure needed |
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Manual Control
|
|
230
|
+
|
|
231
|
+
You rarely need these — sqlacache handles everything through the session automatically. They're available for edge cases:
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
# Cache a statement explicitly
|
|
235
|
+
result = await sqlacache.execute(session, select(User).where(User.active == True), timeout=300)
|
|
236
|
+
|
|
237
|
+
# Invalidate specific rows
|
|
238
|
+
await sqlacache.invalidate(User, pks=[42, 99])
|
|
239
|
+
|
|
240
|
+
# Invalidate all cached reads for a model
|
|
241
|
+
await sqlacache.invalidate(User)
|
|
242
|
+
|
|
243
|
+
# Flush everything
|
|
244
|
+
await sqlacache.invalidate_all()
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## How It Works
|
|
250
|
+
|
|
251
|
+
```text
|
|
252
|
+
session.get(User, 42)
|
|
253
|
+
│
|
|
254
|
+
├── cache HIT → return immediately
|
|
255
|
+
└── cache MISS → execute SQL → tag result as "users:42" → return
|
|
256
|
+
|
|
257
|
+
session.commit() [User id=42 changed]
|
|
258
|
+
│
|
|
259
|
+
└── after_flush event → delete tag "users:42" → all reads that touched that row are evicted
|
|
260
|
+
└── (Redis) → pub/sub → other workers evict their copies too
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
- Cache keys are a hash of the compiled SQL + bound parameters + a per-table version counter.
|
|
264
|
+
- Tags (`"{tablename}:{pk}"`) let cashews atomically evict all keys that depended on a row.
|
|
265
|
+
- Bulk `UPDATE ... WHERE ...` bumps a table-level version so all cached queries for that model go stale.
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## Caveats
|
|
270
|
+
|
|
271
|
+
- **Async only.** Sync `Session` is not supported yet (planned for v0.2.0).
|
|
272
|
+
- **Bulk writes use table-level invalidation.** `session.execute(update(Model).where(...))` evicts all cached reads for that model, not just the affected rows.
|
|
273
|
+
- **Eager-loaded relationships are not tracked.** If a related row changes, queries that joined or selectin-loaded it won't be invalidated.
|
|
274
|
+
- **Raw SQL is not intercepted.** `text(...)` and `engine.execute()` bypass sqlacache entirely.
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Development
|
|
279
|
+
|
|
280
|
+
```bash
|
|
281
|
+
git clone https://github.com/hr-samsami/sqlacache
|
|
282
|
+
cd sqlacache
|
|
283
|
+
uv sync --extra redis --group dev
|
|
284
|
+
|
|
285
|
+
make test # unit tests (no infrastructure needed)
|
|
286
|
+
make lint # ruff
|
|
287
|
+
make format # ruff format
|
|
288
|
+
make typecheck # mypy
|
|
289
|
+
|
|
290
|
+
docker-compose up -d
|
|
291
|
+
make integration # Redis + Postgres integration tests
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
## License
|
|
297
|
+
|
|
298
|
+
[MIT](LICENSE)
|