cache-results 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.
- cache_results-0.1.0/.claude/skills/README.md +73 -0
- cache_results-0.1.0/.gitignore +35 -0
- cache_results-0.1.0/CHANGELOG.md +6 -0
- cache_results-0.1.0/LICENSE +21 -0
- cache_results-0.1.0/PKG-INFO +273 -0
- cache_results-0.1.0/README.md +224 -0
- cache_results-0.1.0/pyproject.toml +84 -0
- cache_results-0.1.0/src/cache_results/__init__.py +24 -0
- cache_results-0.1.0/src/cache_results/__version__.py +3 -0
- cache_results-0.1.0/src/cache_results/conf.py +200 -0
- cache_results-0.1.0/src/cache_results/decorators.py +576 -0
- cache_results-0.1.0/src/cache_results/keys.py +216 -0
- cache_results-0.1.0/src/cache_results/py.typed +0 -0
- cache_results-0.1.0/src/cache_results/sentinels.py +50 -0
- cache_results-0.1.0/tests/__init__.py +0 -0
- cache_results-0.1.0/tests/settings.py +27 -0
- cache_results-0.1.0/tests/test_conf.py +69 -0
- cache_results-0.1.0/tests/test_decorators.py +570 -0
- cache_results-0.1.0/tests/test_keys.py +143 -0
- cache_results-0.1.0/tests/test_package.py +38 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# cache-results Agent Skills
|
|
2
|
+
|
|
3
|
+
This folder holds the [Agent Skills](https://agentskills.io/specification) for
|
|
4
|
+
working on cache-results with an AI coding agent such as Claude Code. This
|
|
5
|
+
README is written mostly for people.
|
|
6
|
+
|
|
7
|
+
A skill is a folder of written know-how that an agent loads only when a task
|
|
8
|
+
needs it. Each one contains a `SKILL.md` file, and sometimes a `references/`
|
|
9
|
+
folder. These skills cover how this repository is built, tested, documented,
|
|
10
|
+
released and committed. They are developer tooling, like the `Makefile` and
|
|
11
|
+
`.pre-commit-config.yaml`, and the package never loads them.
|
|
12
|
+
|
|
13
|
+
They are adapted from the skills of
|
|
14
|
+
[Smarter](https://github.com/smarter-sh/smarter/tree/main/skills), where this
|
|
15
|
+
package's code began, so that both are developed the same way. Smarter's skills
|
|
16
|
+
that only concern its platform (Docker, Kubernetes, React, migrations, ...)
|
|
17
|
+
have no counterpart here. Load them from `../smarter/skills/` when a task leads
|
|
18
|
+
into Smarter, e.g. to make the same change in Smarter's copy of the code.
|
|
19
|
+
|
|
20
|
+
## What's here
|
|
21
|
+
|
|
22
|
+
Start with `cache-results-development`. It sets out the rules that apply to
|
|
23
|
+
every change, the package's architecture, and which narrower skill to load.
|
|
24
|
+
|
|
25
|
+
| Skill | Use it when |
|
|
26
|
+
| ---------------------------- | -------------------------------------------------------------------------------- |
|
|
27
|
+
| `cache-results-development` | You're making any change. Start here. |
|
|
28
|
+
| `cache-results-testing` | You're writing or running tests, against either cache, or raising coverage. |
|
|
29
|
+
| `cache-results-docstrings` | You're writing docstrings, which become the api reference. |
|
|
30
|
+
| `cache-results-typing` | You're writing type annotations, especially with `Any`. |
|
|
31
|
+
| `cache-results-code-quality` | You're about to finish a change and need to run the pre-commit hooks. |
|
|
32
|
+
| `cache-results-git-commits` | You've been asked to write a commit message. |
|
|
33
|
+
| `cache-results-release` | You're working on CI, Codecov, requirements, semantic-release or PyPI. |
|
|
34
|
+
| `cache-results-sphinx-docs` | You're editing the docs or ADRs, publishing the site, or changing its Terraform. |
|
|
35
|
+
|
|
36
|
+
## Claude Code
|
|
37
|
+
|
|
38
|
+
Claude Code looks for project skills in `.claude/skills/`. This repository
|
|
39
|
+
connects the two folders with a committed symlink, `.claude/skills -> ../skills`,
|
|
40
|
+
as Smarter's does:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
mkdir -p .claude
|
|
44
|
+
ln -s ../skills .claude/skills
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Other agents that read `SKILL.md` folders can be pointed at this folder, or told
|
|
48
|
+
to read `skills/cache-results-development/SKILL.md` at the start of a session.
|
|
49
|
+
|
|
50
|
+
## When to add or change a skill
|
|
51
|
+
|
|
52
|
+
Add to a skill when you've explained the same thing to an agent twice, when a
|
|
53
|
+
mistake is costly (e.g. a change that makes every cached entry unreadable), when
|
|
54
|
+
a pattern isn't obvious from the code, or when checking a change takes a fragile
|
|
55
|
+
series of steps. Don't add one for what the code, the linters or CI already
|
|
56
|
+
enforce, for one-off facts, or for personal preferences. Prefer editing an
|
|
57
|
+
existing skill to adding one, and keep each `SKILL.md` short.
|
|
58
|
+
|
|
59
|
+
## Security considerations
|
|
60
|
+
|
|
61
|
+
A skill is a set of instructions that an agent follows, often without asking.
|
|
62
|
+
**Review a change to this folder as carefully as a change to CI.**
|
|
63
|
+
|
|
64
|
+
- **Some instructions forbid things on purpose**, e.g. never running
|
|
65
|
+
`git commit`, or applying Terraform without a plan the user has seen. Don't
|
|
66
|
+
remove a rule like that unless you mean to give agents that power.
|
|
67
|
+
- **The docs site's Terraform changes live AWS resources** in a shared account
|
|
68
|
+
and DNS zone. The skills require a reviewed plan before every apply.
|
|
69
|
+
- **Skills can carry prompt injection.** Look for instructions unrelated to a
|
|
70
|
+
skill's purpose, new scripts with network calls, and text that tells an agent
|
|
71
|
+
to skip confirmations, hooks or safety rules.
|
|
72
|
+
- **Keep secrets out.** This repository is public: no credentials, account ids,
|
|
73
|
+
private hostnames or customer data in a skill or an example.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# virtual environments
|
|
2
|
+
.venv/
|
|
3
|
+
venv/
|
|
4
|
+
|
|
5
|
+
# Python
|
|
6
|
+
__pycache__/
|
|
7
|
+
*.py[cod]
|
|
8
|
+
*.egg-info/
|
|
9
|
+
build/
|
|
10
|
+
dist/
|
|
11
|
+
|
|
12
|
+
# Sphinx
|
|
13
|
+
docs/build/
|
|
14
|
+
|
|
15
|
+
# Terraform: local state, plans and providers. .terraform.lock.hcl is committed.
|
|
16
|
+
.terraform/
|
|
17
|
+
*.tfstate
|
|
18
|
+
*.tfstate.*
|
|
19
|
+
*.tfplan
|
|
20
|
+
tfplan
|
|
21
|
+
|
|
22
|
+
# tools
|
|
23
|
+
.coverage
|
|
24
|
+
.coverage.*
|
|
25
|
+
coverage.xml
|
|
26
|
+
htmlcov/
|
|
27
|
+
.pytest_cache/
|
|
28
|
+
.mypy_cache/
|
|
29
|
+
|
|
30
|
+
# editors and operating systems. .vscode/ is committed: it points VS Code at venv/.
|
|
31
|
+
.DS_Store
|
|
32
|
+
.idea/
|
|
33
|
+
|
|
34
|
+
# node, for the commitlint hook
|
|
35
|
+
node_modules/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lawrence McDaniel
|
|
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.
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: cache-results
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Django decorator that caches a function's results by its arguments, with safe keys, async support and invalidation.
|
|
5
|
+
Project-URL: Homepage, https://github.com/smarter-sh/cache-results
|
|
6
|
+
Project-URL: Source, https://github.com/smarter-sh/cache-results
|
|
7
|
+
Project-URL: Issues, https://github.com/smarter-sh/cache-results/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/smarter-sh/cache-results/blob/main/CHANGELOG.md
|
|
9
|
+
Author-email: Lawrence McDaniel <lpm0073@gmail.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: cache,caching,decorator,django,memoize,redis
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Environment :: Web Environment
|
|
15
|
+
Classifier: Framework :: Django
|
|
16
|
+
Classifier: Framework :: Django :: 4.2
|
|
17
|
+
Classifier: Framework :: Django :: 5.2
|
|
18
|
+
Classifier: Framework :: Django :: 6.0
|
|
19
|
+
Classifier: Intended Audience :: Developers
|
|
20
|
+
Classifier: Operating System :: OS Independent
|
|
21
|
+
Classifier: Programming Language :: Python :: 3
|
|
22
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
27
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
28
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
29
|
+
Classifier: Typing :: Typed
|
|
30
|
+
Requires-Python: >=3.10
|
|
31
|
+
Requires-Dist: asgiref>=3.6
|
|
32
|
+
Requires-Dist: django>=4.2
|
|
33
|
+
Provides-Extra: docs
|
|
34
|
+
Requires-Dist: sphinx-copybutton>=0.5; extra == 'docs'
|
|
35
|
+
Requires-Dist: sphinx-notfound-page>=1; extra == 'docs'
|
|
36
|
+
Requires-Dist: sphinx-rtd-theme>=3; extra == 'docs'
|
|
37
|
+
Requires-Dist: sphinx-sitemap>=2.6; extra == 'docs'
|
|
38
|
+
Requires-Dist: sphinx>=8; extra == 'docs'
|
|
39
|
+
Provides-Extra: redis
|
|
40
|
+
Requires-Dist: django-redis>=5.4; extra == 'redis'
|
|
41
|
+
Provides-Extra: test
|
|
42
|
+
Requires-Dist: coverage[toml]>=7; extra == 'test'
|
|
43
|
+
Requires-Dist: pytest-django>=4.9; extra == 'test'
|
|
44
|
+
Requires-Dist: pytest>=8; extra == 'test'
|
|
45
|
+
Provides-Extra: typing
|
|
46
|
+
Requires-Dist: django-stubs>=5; extra == 'typing'
|
|
47
|
+
Requires-Dist: mypy>=1.10; extra == 'typing'
|
|
48
|
+
Description-Content-Type: text/markdown
|
|
49
|
+
|
|
50
|
+
# cache-results
|
|
51
|
+
|
|
52
|
+
[](https://github.com/smarter-sh/cache-results/actions/workflows/ci.yml)
|
|
53
|
+
[](https://codecov.io/gh/smarter-sh/cache-results)
|
|
54
|
+
[](https://github.com/smarter-sh/cache-results/releases)
|
|
55
|
+
[](https://www.python.org/)
|
|
56
|
+
[](https://www.djangoproject.com/)
|
|
57
|
+
[](LICENSE)
|
|
58
|
+
[](https://docs.cache-results.smarter.sh/)
|
|
59
|
+
|
|
60
|
+
A Django decorator that caches a function's results by its arguments, in any
|
|
61
|
+
Django cache, such as Redis.
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from cache_results import cache_results
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@cache_results(timeout=600)
|
|
68
|
+
def get_book(book_id: int) -> dict:
|
|
69
|
+
return Book.objects.values().get(pk=book_id)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
get_book(42) # computed and cached
|
|
73
|
+
get_book(42) # served from the cache
|
|
74
|
+
get_book.invalidate(42) # clears that entry
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Documentation: <https://docs.cache-results.smarter.sh/>**
|
|
78
|
+
|
|
79
|
+
cache-results grew out of the caching layer of
|
|
80
|
+
[Smarter](https://github.com/smarter-sh/smarter), where it caches database
|
|
81
|
+
reads and third-party api calls across many application servers.
|
|
82
|
+
|
|
83
|
+
## Features
|
|
84
|
+
|
|
85
|
+
- **Safe keys.** The key is the function's module and qualified name, plus a
|
|
86
|
+
hash of its arguments bound to its signature, so `f(1)`, `f(a=1)` and
|
|
87
|
+
`f(1, b=2)` (when `b` defaults to 2) share one entry. Arguments that can't
|
|
88
|
+
be part of a safe key, such as model instances, run the function uncached and
|
|
89
|
+
log an error, rather than be keyed by `str()`.
|
|
90
|
+
- **`None` is cached** like any other result.
|
|
91
|
+
- **`.invalidate(\*args, **kwargs)`\*\* on every decorated function clears the
|
|
92
|
+
entry of those arguments.
|
|
93
|
+
- **async functions** are cached without blocking the event loop.
|
|
94
|
+
- **Classmethods** are cached per class. Instance methods are refused, because
|
|
95
|
+
`self` would be part of the key.
|
|
96
|
+
- **No stampede within a process.** Threads that miss the same key at once
|
|
97
|
+
wait for the first one to compute it.
|
|
98
|
+
- **Safe during startup.** A function called before Django's apps are ready
|
|
99
|
+
runs uncached, rather than initialize the cache framework too early.
|
|
100
|
+
- **Configurable** through one Django setting, `CACHE_RESULTS`.
|
|
101
|
+
- Depends only on Django and asgiref.
|
|
102
|
+
|
|
103
|
+
## Installation
|
|
104
|
+
|
|
105
|
+
cache-results is not on PyPI yet. Until it is, install it from GitHub:
|
|
106
|
+
|
|
107
|
+
```console
|
|
108
|
+
pip install "cache-results @ git+https://github.com/smarter-sh/cache-results"
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
It needs Python 3.10 or newer and Django 4.2 or newer, and works with any
|
|
112
|
+
Django cache backend. With a shared cache such as Redis, cached results
|
|
113
|
+
survive restarts and are shared by every application server.
|
|
114
|
+
|
|
115
|
+
## Arguments
|
|
116
|
+
|
|
117
|
+
The arguments are the cache key, so they must be plain values: `str`, `int`,
|
|
118
|
+
`float`, `bool`, `None`, and lists, tuples, dicts and sets of them, plus
|
|
119
|
+
datetimes, Decimals, UUIDs, classes and dataclasses. Pass a model's id, not the
|
|
120
|
+
model:
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
@cache_results()
|
|
124
|
+
def get_profile(user_id: int) -> Profile:
|
|
125
|
+
return Profile.objects.select_related("user").get(user_id=user_id)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
get_profile(request.user.pk)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
To allow another type, subclass `CacheKeyEncoder` and name your subclass in
|
|
132
|
+
the `KEY_ENCODER` setting:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from cache_results import CacheKeyEncoder
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class MyCacheKeyEncoder(CacheKeyEncoder):
|
|
139
|
+
def default(self, o):
|
|
140
|
+
if isinstance(o, Money):
|
|
141
|
+
return {"__money__": [str(o.amount), o.currency]}
|
|
142
|
+
return super().default(o)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Never pass a secret, such as an api key, as an argument: arguments are part
|
|
146
|
+
of the key, and verbose logging writes them to the log. Pass the id of the
|
|
147
|
+
record that holds it, and read the secret inside the function.
|
|
148
|
+
|
|
149
|
+
### Functions defined inside an inherited classmethod
|
|
150
|
+
|
|
151
|
+
A qualified name records where a function is defined, not who calls it. A
|
|
152
|
+
function defined inside a classmethod that subclasses inherit has the same
|
|
153
|
+
qualified name for every subclass, so subclasses would share its entries.
|
|
154
|
+
Give it the class name as a parameter:
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
class Resource(models.Model):
|
|
158
|
+
class Meta:
|
|
159
|
+
abstract = True
|
|
160
|
+
|
|
161
|
+
@classmethod
|
|
162
|
+
def get_cached(cls, pk: int):
|
|
163
|
+
@cache_results()
|
|
164
|
+
def _get(pk: int, class_name: str = cls.__name__):
|
|
165
|
+
return cls.objects.get(pk=pk)
|
|
166
|
+
|
|
167
|
+
return _get(pk)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Settings
|
|
171
|
+
|
|
172
|
+
Every entry of the `CACHE_RESULTS` setting is optional:
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
CACHE_RESULTS = {
|
|
176
|
+
# the CACHES alias that holds cached results.
|
|
177
|
+
"CACHE_ALIAS": "default",
|
|
178
|
+
# the default timeout, in seconds, of a decorated function that names none.
|
|
179
|
+
# Unset, it is the cache alias's own TIMEOUT.
|
|
180
|
+
"TIMEOUT": 3600,
|
|
181
|
+
# text that starts every key. Changing it, or VERSION, abandons every cached
|
|
182
|
+
# result at once, without touching anything else in the same cache.
|
|
183
|
+
"KEY_PREFIX": "",
|
|
184
|
+
"VERSION": None,
|
|
185
|
+
# a subclass of cache_results.CacheKeyEncoder, to allow more argument types.
|
|
186
|
+
"KEY_ENCODER": "cache_results.keys.CacheKeyEncoder",
|
|
187
|
+
# zero-argument callables that return whether to log cache hits, misses and
|
|
188
|
+
# invalidations, and whether those logs include the arguments.
|
|
189
|
+
"LOGGING_ENABLED": None,
|
|
190
|
+
"VERBOSE_LOGGING": None,
|
|
191
|
+
# a callable(text, event) that formats the start of each log message, where
|
|
192
|
+
# event is "hit", "miss", "invalidate" or "".
|
|
193
|
+
"LOG_PREFIX": None,
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Callables and classes are given as dotted paths or as the objects themselves.
|
|
198
|
+
Settings are read on every call, so `override_settings` works in tests.
|
|
199
|
+
|
|
200
|
+
## Decorator options
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
@cache_results(timeout=None, cache_key=None, logging_enabled=False)
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
- `timeout`: seconds. `None` uses the `TIMEOUT` setting, and `0` caches
|
|
207
|
+
nothing.
|
|
208
|
+
- `cache_key`: a fixed key that every call shares, whatever its arguments.
|
|
209
|
+
Use it only for functions whose result doesn't depend on them.
|
|
210
|
+
- `logging_enabled`: log this function's hits, misses and invalidations even
|
|
211
|
+
when the `LOGGING_ENABLED` setting is off. Logs name the function and the
|
|
212
|
+
type of its result, never the result.
|
|
213
|
+
|
|
214
|
+
## Development
|
|
215
|
+
|
|
216
|
+
```console
|
|
217
|
+
make # list every command
|
|
218
|
+
make init # create venv/ from requirements/local.txt, install the package and the git hooks
|
|
219
|
+
make test # run the tests against the local-memory cache
|
|
220
|
+
make test-redis # run the tests against Redis
|
|
221
|
+
make coverage # run the tests with a coverage report
|
|
222
|
+
make lint # run the pre-commit hooks on every file
|
|
223
|
+
make typecheck # check the type annotations with mypy
|
|
224
|
+
make sphinx-build # build the Sphinx docs into docs/build/html
|
|
225
|
+
make sphinx-publish # publish them to docs.cache-results.smarter.sh
|
|
226
|
+
make build # build the sdist and wheel, and check them
|
|
227
|
+
make python-requirements # recompile the pinned requirements/*.txt
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
`make test-redis` uses `redis://localhost:6379/15` unless you set
|
|
231
|
+
`REDIS_URL`, and clears that database. Running pytest yourself, set
|
|
232
|
+
`CACHE_RESULTS_TEST_REDIS_URL` instead.
|
|
233
|
+
|
|
234
|
+
CI runs the hooks, the tests on every supported pair of Python and Django
|
|
235
|
+
versions and against Redis, and a build, on every push and pull request.
|
|
236
|
+
Coverage is reported to [Codecov](https://codecov.io/gh/smarter-sh/cache-results).
|
|
237
|
+
Releases are made by the Release workflow with semantic-release, from the
|
|
238
|
+
conventional commit messages (`make release-dry-run` shows the next version).
|
|
239
|
+
|
|
240
|
+
## Contributing
|
|
241
|
+
|
|
242
|
+
Bug reports, questions and pull requests are welcome. Please
|
|
243
|
+
[open an issue](https://github.com/smarter-sh/cache-results/issues) before
|
|
244
|
+
starting on a larger change, so that we can agree on its design first.
|
|
245
|
+
|
|
246
|
+
1. Fork the repository, and branch from `main`.
|
|
247
|
+
2. Run `make init`. It also installs the pre-commit hooks, which are the lint
|
|
248
|
+
standard (black, flake8, isort, pyupgrade, codespell, bandit, Prettier) and
|
|
249
|
+
check each commit message.
|
|
250
|
+
3. Make your change with its tests. `make coverage` must stay at 95% or
|
|
251
|
+
more, and both `make test` and `make test-redis` must pass. CI checks all
|
|
252
|
+
of this on your pull request.
|
|
253
|
+
4. Write [conventional commits](https://www.conventionalcommits.org/), e.g.
|
|
254
|
+
`fix(keys): sort frozensets of tuples`. Releases are versioned from them:
|
|
255
|
+
`fix` makes a patch release, `feat` a minor one.
|
|
256
|
+
5. Open a pull request that says what changed and why, and links its issue.
|
|
257
|
+
|
|
258
|
+
A few rules keep @cache_results() safe and readable:
|
|
259
|
+
|
|
260
|
+
- The package depends only on Django and asgiref. Project-specific behavior
|
|
261
|
+
belongs in a project's own hooks, named by the `CACHE_RESULTS` setting.
|
|
262
|
+
- Arguments are never keyed by `str()`, and results and secrets are never
|
|
263
|
+
logged.
|
|
264
|
+
- A change to the key format, or to the value stored for `None`, makes every
|
|
265
|
+
cached entry unreadable, so it needs a major release.
|
|
266
|
+
|
|
267
|
+
Governance of the Smarter projects is described in
|
|
268
|
+
[smarter-sh/community](https://github.com/smarter-sh/community). Everyone who
|
|
269
|
+
takes part is expected to follow the [code of conduct](CODE_OF_CONDUCT.md).
|
|
270
|
+
|
|
271
|
+
## License
|
|
272
|
+
|
|
273
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# cache-results
|
|
2
|
+
|
|
3
|
+
[](https://github.com/smarter-sh/cache-results/actions/workflows/ci.yml)
|
|
4
|
+
[](https://codecov.io/gh/smarter-sh/cache-results)
|
|
5
|
+
[](https://github.com/smarter-sh/cache-results/releases)
|
|
6
|
+
[](https://www.python.org/)
|
|
7
|
+
[](https://www.djangoproject.com/)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
[](https://docs.cache-results.smarter.sh/)
|
|
10
|
+
|
|
11
|
+
A Django decorator that caches a function's results by its arguments, in any
|
|
12
|
+
Django cache, such as Redis.
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
from cache_results import cache_results
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@cache_results(timeout=600)
|
|
19
|
+
def get_book(book_id: int) -> dict:
|
|
20
|
+
return Book.objects.values().get(pk=book_id)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
get_book(42) # computed and cached
|
|
24
|
+
get_book(42) # served from the cache
|
|
25
|
+
get_book.invalidate(42) # clears that entry
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Documentation: <https://docs.cache-results.smarter.sh/>**
|
|
29
|
+
|
|
30
|
+
cache-results grew out of the caching layer of
|
|
31
|
+
[Smarter](https://github.com/smarter-sh/smarter), where it caches database
|
|
32
|
+
reads and third-party api calls across many application servers.
|
|
33
|
+
|
|
34
|
+
## Features
|
|
35
|
+
|
|
36
|
+
- **Safe keys.** The key is the function's module and qualified name, plus a
|
|
37
|
+
hash of its arguments bound to its signature, so `f(1)`, `f(a=1)` and
|
|
38
|
+
`f(1, b=2)` (when `b` defaults to 2) share one entry. Arguments that can't
|
|
39
|
+
be part of a safe key, such as model instances, run the function uncached and
|
|
40
|
+
log an error, rather than be keyed by `str()`.
|
|
41
|
+
- **`None` is cached** like any other result.
|
|
42
|
+
- **`.invalidate(\*args, **kwargs)`\*\* on every decorated function clears the
|
|
43
|
+
entry of those arguments.
|
|
44
|
+
- **async functions** are cached without blocking the event loop.
|
|
45
|
+
- **Classmethods** are cached per class. Instance methods are refused, because
|
|
46
|
+
`self` would be part of the key.
|
|
47
|
+
- **No stampede within a process.** Threads that miss the same key at once
|
|
48
|
+
wait for the first one to compute it.
|
|
49
|
+
- **Safe during startup.** A function called before Django's apps are ready
|
|
50
|
+
runs uncached, rather than initialize the cache framework too early.
|
|
51
|
+
- **Configurable** through one Django setting, `CACHE_RESULTS`.
|
|
52
|
+
- Depends only on Django and asgiref.
|
|
53
|
+
|
|
54
|
+
## Installation
|
|
55
|
+
|
|
56
|
+
cache-results is not on PyPI yet. Until it is, install it from GitHub:
|
|
57
|
+
|
|
58
|
+
```console
|
|
59
|
+
pip install "cache-results @ git+https://github.com/smarter-sh/cache-results"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
It needs Python 3.10 or newer and Django 4.2 or newer, and works with any
|
|
63
|
+
Django cache backend. With a shared cache such as Redis, cached results
|
|
64
|
+
survive restarts and are shared by every application server.
|
|
65
|
+
|
|
66
|
+
## Arguments
|
|
67
|
+
|
|
68
|
+
The arguments are the cache key, so they must be plain values: `str`, `int`,
|
|
69
|
+
`float`, `bool`, `None`, and lists, tuples, dicts and sets of them, plus
|
|
70
|
+
datetimes, Decimals, UUIDs, classes and dataclasses. Pass a model's id, not the
|
|
71
|
+
model:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
@cache_results()
|
|
75
|
+
def get_profile(user_id: int) -> Profile:
|
|
76
|
+
return Profile.objects.select_related("user").get(user_id=user_id)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
get_profile(request.user.pk)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
To allow another type, subclass `CacheKeyEncoder` and name your subclass in
|
|
83
|
+
the `KEY_ENCODER` setting:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from cache_results import CacheKeyEncoder
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class MyCacheKeyEncoder(CacheKeyEncoder):
|
|
90
|
+
def default(self, o):
|
|
91
|
+
if isinstance(o, Money):
|
|
92
|
+
return {"__money__": [str(o.amount), o.currency]}
|
|
93
|
+
return super().default(o)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Never pass a secret, such as an api key, as an argument: arguments are part
|
|
97
|
+
of the key, and verbose logging writes them to the log. Pass the id of the
|
|
98
|
+
record that holds it, and read the secret inside the function.
|
|
99
|
+
|
|
100
|
+
### Functions defined inside an inherited classmethod
|
|
101
|
+
|
|
102
|
+
A qualified name records where a function is defined, not who calls it. A
|
|
103
|
+
function defined inside a classmethod that subclasses inherit has the same
|
|
104
|
+
qualified name for every subclass, so subclasses would share its entries.
|
|
105
|
+
Give it the class name as a parameter:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
class Resource(models.Model):
|
|
109
|
+
class Meta:
|
|
110
|
+
abstract = True
|
|
111
|
+
|
|
112
|
+
@classmethod
|
|
113
|
+
def get_cached(cls, pk: int):
|
|
114
|
+
@cache_results()
|
|
115
|
+
def _get(pk: int, class_name: str = cls.__name__):
|
|
116
|
+
return cls.objects.get(pk=pk)
|
|
117
|
+
|
|
118
|
+
return _get(pk)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Settings
|
|
122
|
+
|
|
123
|
+
Every entry of the `CACHE_RESULTS` setting is optional:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
CACHE_RESULTS = {
|
|
127
|
+
# the CACHES alias that holds cached results.
|
|
128
|
+
"CACHE_ALIAS": "default",
|
|
129
|
+
# the default timeout, in seconds, of a decorated function that names none.
|
|
130
|
+
# Unset, it is the cache alias's own TIMEOUT.
|
|
131
|
+
"TIMEOUT": 3600,
|
|
132
|
+
# text that starts every key. Changing it, or VERSION, abandons every cached
|
|
133
|
+
# result at once, without touching anything else in the same cache.
|
|
134
|
+
"KEY_PREFIX": "",
|
|
135
|
+
"VERSION": None,
|
|
136
|
+
# a subclass of cache_results.CacheKeyEncoder, to allow more argument types.
|
|
137
|
+
"KEY_ENCODER": "cache_results.keys.CacheKeyEncoder",
|
|
138
|
+
# zero-argument callables that return whether to log cache hits, misses and
|
|
139
|
+
# invalidations, and whether those logs include the arguments.
|
|
140
|
+
"LOGGING_ENABLED": None,
|
|
141
|
+
"VERBOSE_LOGGING": None,
|
|
142
|
+
# a callable(text, event) that formats the start of each log message, where
|
|
143
|
+
# event is "hit", "miss", "invalidate" or "".
|
|
144
|
+
"LOG_PREFIX": None,
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Callables and classes are given as dotted paths or as the objects themselves.
|
|
149
|
+
Settings are read on every call, so `override_settings` works in tests.
|
|
150
|
+
|
|
151
|
+
## Decorator options
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
@cache_results(timeout=None, cache_key=None, logging_enabled=False)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
- `timeout`: seconds. `None` uses the `TIMEOUT` setting, and `0` caches
|
|
158
|
+
nothing.
|
|
159
|
+
- `cache_key`: a fixed key that every call shares, whatever its arguments.
|
|
160
|
+
Use it only for functions whose result doesn't depend on them.
|
|
161
|
+
- `logging_enabled`: log this function's hits, misses and invalidations even
|
|
162
|
+
when the `LOGGING_ENABLED` setting is off. Logs name the function and the
|
|
163
|
+
type of its result, never the result.
|
|
164
|
+
|
|
165
|
+
## Development
|
|
166
|
+
|
|
167
|
+
```console
|
|
168
|
+
make # list every command
|
|
169
|
+
make init # create venv/ from requirements/local.txt, install the package and the git hooks
|
|
170
|
+
make test # run the tests against the local-memory cache
|
|
171
|
+
make test-redis # run the tests against Redis
|
|
172
|
+
make coverage # run the tests with a coverage report
|
|
173
|
+
make lint # run the pre-commit hooks on every file
|
|
174
|
+
make typecheck # check the type annotations with mypy
|
|
175
|
+
make sphinx-build # build the Sphinx docs into docs/build/html
|
|
176
|
+
make sphinx-publish # publish them to docs.cache-results.smarter.sh
|
|
177
|
+
make build # build the sdist and wheel, and check them
|
|
178
|
+
make python-requirements # recompile the pinned requirements/*.txt
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
`make test-redis` uses `redis://localhost:6379/15` unless you set
|
|
182
|
+
`REDIS_URL`, and clears that database. Running pytest yourself, set
|
|
183
|
+
`CACHE_RESULTS_TEST_REDIS_URL` instead.
|
|
184
|
+
|
|
185
|
+
CI runs the hooks, the tests on every supported pair of Python and Django
|
|
186
|
+
versions and against Redis, and a build, on every push and pull request.
|
|
187
|
+
Coverage is reported to [Codecov](https://codecov.io/gh/smarter-sh/cache-results).
|
|
188
|
+
Releases are made by the Release workflow with semantic-release, from the
|
|
189
|
+
conventional commit messages (`make release-dry-run` shows the next version).
|
|
190
|
+
|
|
191
|
+
## Contributing
|
|
192
|
+
|
|
193
|
+
Bug reports, questions and pull requests are welcome. Please
|
|
194
|
+
[open an issue](https://github.com/smarter-sh/cache-results/issues) before
|
|
195
|
+
starting on a larger change, so that we can agree on its design first.
|
|
196
|
+
|
|
197
|
+
1. Fork the repository, and branch from `main`.
|
|
198
|
+
2. Run `make init`. It also installs the pre-commit hooks, which are the lint
|
|
199
|
+
standard (black, flake8, isort, pyupgrade, codespell, bandit, Prettier) and
|
|
200
|
+
check each commit message.
|
|
201
|
+
3. Make your change with its tests. `make coverage` must stay at 95% or
|
|
202
|
+
more, and both `make test` and `make test-redis` must pass. CI checks all
|
|
203
|
+
of this on your pull request.
|
|
204
|
+
4. Write [conventional commits](https://www.conventionalcommits.org/), e.g.
|
|
205
|
+
`fix(keys): sort frozensets of tuples`. Releases are versioned from them:
|
|
206
|
+
`fix` makes a patch release, `feat` a minor one.
|
|
207
|
+
5. Open a pull request that says what changed and why, and links its issue.
|
|
208
|
+
|
|
209
|
+
A few rules keep @cache_results() safe and readable:
|
|
210
|
+
|
|
211
|
+
- The package depends only on Django and asgiref. Project-specific behavior
|
|
212
|
+
belongs in a project's own hooks, named by the `CACHE_RESULTS` setting.
|
|
213
|
+
- Arguments are never keyed by `str()`, and results and secrets are never
|
|
214
|
+
logged.
|
|
215
|
+
- A change to the key format, or to the value stored for `None`, makes every
|
|
216
|
+
cached entry unreadable, so it needs a major release.
|
|
217
|
+
|
|
218
|
+
Governance of the Smarter projects is described in
|
|
219
|
+
[smarter-sh/community](https://github.com/smarter-sh/community). Everyone who
|
|
220
|
+
takes part is expected to follow the [code of conduct](CODE_OF_CONDUCT.md).
|
|
221
|
+
|
|
222
|
+
## License
|
|
223
|
+
|
|
224
|
+
[MIT](LICENSE)
|