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.
@@ -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,6 @@
1
+ # [0.1.0](https://github.com/smarter-sh/cache-results/compare/v0.0.0...v0.1.0) (2026-10-25)
2
+
3
+
4
+ ### Features
5
+
6
+ * port the cache_results decorator from Smarter ([5357102](https://github.com/smarter-sh/cache-results/commit/5357102b5fb7c80fa4022eda7b5b95b1c3e2dee2))
@@ -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
+ [![CI](https://github.com/smarter-sh/cache-results/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/smarter-sh/cache-results/actions/workflows/ci.yml)
53
+ [![codecov](https://codecov.io/gh/smarter-sh/cache-results/graph/badge.svg)](https://codecov.io/gh/smarter-sh/cache-results)
54
+ [![Latest Release](https://img.shields.io/github/v/release/smarter-sh/cache-results?label=release)](https://github.com/smarter-sh/cache-results/releases)
55
+ [![Python](https://img.shields.io/badge/python-3.10%2B-blue?logo=python&logoColor=white)](https://www.python.org/)
56
+ [![Django](https://img.shields.io/badge/django-4.2%2B-0C4B33?logo=django&logoColor=white)](https://www.djangoproject.com/)
57
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
58
+ [![Docs](https://img.shields.io/badge/docs-docs.cache--results.smarter.sh-blue?logo=readthedocs)](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
+ [![CI](https://github.com/smarter-sh/cache-results/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/smarter-sh/cache-results/actions/workflows/ci.yml)
4
+ [![codecov](https://codecov.io/gh/smarter-sh/cache-results/graph/badge.svg)](https://codecov.io/gh/smarter-sh/cache-results)
5
+ [![Latest Release](https://img.shields.io/github/v/release/smarter-sh/cache-results?label=release)](https://github.com/smarter-sh/cache-results/releases)
6
+ [![Python](https://img.shields.io/badge/python-3.10%2B-blue?logo=python&logoColor=white)](https://www.python.org/)
7
+ [![Django](https://img.shields.io/badge/django-4.2%2B-0C4B33?logo=django&logoColor=white)](https://www.djangoproject.com/)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
9
+ [![Docs](https://img.shields.io/badge/docs-docs.cache--results.smarter.sh-blue?logo=readthedocs)](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)