django-stash 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.
- django_stash-0.1.0/LICENSE +28 -0
- django_stash-0.1.0/PKG-INFO +110 -0
- django_stash-0.1.0/README.md +75 -0
- django_stash-0.1.0/pyproject.toml +67 -0
- django_stash-0.1.0/src/stash/__init__.py +28 -0
- django_stash-0.1.0/src/stash/__version__.py +1 -0
- django_stash-0.1.0/src/stash/api.py +157 -0
- django_stash-0.1.0/src/stash/middleware.py +27 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Andy Babic
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: django-stash
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Scoped ambient memoization for Django — stash values for the duration of a request or task, available anywhere in the stack.
|
|
5
|
+
Keywords: Django,cache,memoize,request,asgiref,performance
|
|
6
|
+
Author-email: Andy Babic <andyjbabic@gmail.com>
|
|
7
|
+
Maintainer-email: Andy Babic <andyjbabic@gmail.com>
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Framework :: Django
|
|
20
|
+
Classifier: Framework :: Django :: 4.2
|
|
21
|
+
Classifier: Framework :: Django :: 5.0
|
|
22
|
+
Classifier: Framework :: Django :: 5.1
|
|
23
|
+
Classifier: Framework :: Django :: 5.2
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: Django>=4.2
|
|
26
|
+
Requires-Dist: asgiref>=3.7
|
|
27
|
+
Requires-Dist: ruff>=0.1.1,<1.0 ; extra == "development"
|
|
28
|
+
Requires-Dist: coverage>=7.0,<8.0 ; extra == "testing"
|
|
29
|
+
Project-URL: Changelog, https://github.com/ababic/django-stash/blob/main/CHANGELOG.md
|
|
30
|
+
Project-URL: Source, https://github.com/ababic/django-stash
|
|
31
|
+
Provides-Extra: development
|
|
32
|
+
Provides-Extra: testing
|
|
33
|
+
Import-Name: stash
|
|
34
|
+
|
|
35
|
+
# django-stash
|
|
36
|
+
|
|
37
|
+
[](https://opensource.org/licenses/BSD-3-Clause)
|
|
38
|
+
[](https://github.com/astral-sh/ruff)
|
|
39
|
+
|
|
40
|
+
Scoped ambient memoization for Django.
|
|
41
|
+
|
|
42
|
+
Stash values for the duration of a **request** (or any other explicit scope), and read them again from anywhere in the call stack — without threading a `request` through every helper.
|
|
43
|
+
|
|
44
|
+
This is **not** a cache backend. It does not replace Redis/Memcached/database cache. It is an L1 memo pad that only exists while a scope is open.
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install django-stash
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
MIDDLEWARE = [
|
|
54
|
+
"stash.middleware.StashMiddleware",
|
|
55
|
+
# ...
|
|
56
|
+
]
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
No `INSTALLED_APPS` entry required.
|
|
60
|
+
|
|
61
|
+
## Usage
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
import stash
|
|
65
|
+
|
|
66
|
+
def expensive_site_paths():
|
|
67
|
+
return stash.get_or_set(
|
|
68
|
+
"wagtail.site_root_paths",
|
|
69
|
+
loader=compute_site_root_paths,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
@stash.memoize(key="taxonomy.colours")
|
|
73
|
+
def get_colour_choices():
|
|
74
|
+
return list(Colour.objects.values_list("id", "name"))
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Outside a scope (management commands, Celery, shell), `get_or_set` / `memoize` just call the loader every time. Nothing sticks on the worker.
|
|
78
|
+
|
|
79
|
+
### Manual scopes
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
with stash.stash_scope():
|
|
83
|
+
...
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Useful for tests, management commands, or task bodies that want the same behaviour.
|
|
87
|
+
|
|
88
|
+
### Clearing
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
stash.clear("wagtail.site_root_paths") # one key
|
|
92
|
+
stash.clear() # everything in this scope
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Rules of thumb
|
|
96
|
+
|
|
97
|
+
1. **Scope owns lifetime** — middleware / `stash_scope()` enable at enter and disable in `finally`.
|
|
98
|
+
2. **No scope ⇒ no stash** — avoids sticky process caches on Gunicorn sync workers.
|
|
99
|
+
3. **Prefer immutable-ish values** — stash stores/returns shallow copies; don't stash live model graphs you plan to mutate in place.
|
|
100
|
+
4. **Shared cache is separate** — if you need cross-process invalidation, keep Django's cache as L2 and use stash as L1 in front of it.
|
|
101
|
+
|
|
102
|
+
## Development
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
cd django-stash
|
|
106
|
+
python -m venv .venv && source .venv/bin/activate
|
|
107
|
+
pip install -e ".[testing,development]"
|
|
108
|
+
python testmanage.py test
|
|
109
|
+
```
|
|
110
|
+
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# django-stash
|
|
2
|
+
|
|
3
|
+
[](https://opensource.org/licenses/BSD-3-Clause)
|
|
4
|
+
[](https://github.com/astral-sh/ruff)
|
|
5
|
+
|
|
6
|
+
Scoped ambient memoization for Django.
|
|
7
|
+
|
|
8
|
+
Stash values for the duration of a **request** (or any other explicit scope), and read them again from anywhere in the call stack — without threading a `request` through every helper.
|
|
9
|
+
|
|
10
|
+
This is **not** a cache backend. It does not replace Redis/Memcached/database cache. It is an L1 memo pad that only exists while a scope is open.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install django-stash
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
MIDDLEWARE = [
|
|
20
|
+
"stash.middleware.StashMiddleware",
|
|
21
|
+
# ...
|
|
22
|
+
]
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
No `INSTALLED_APPS` entry required.
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
import stash
|
|
31
|
+
|
|
32
|
+
def expensive_site_paths():
|
|
33
|
+
return stash.get_or_set(
|
|
34
|
+
"wagtail.site_root_paths",
|
|
35
|
+
loader=compute_site_root_paths,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
@stash.memoize(key="taxonomy.colours")
|
|
39
|
+
def get_colour_choices():
|
|
40
|
+
return list(Colour.objects.values_list("id", "name"))
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Outside a scope (management commands, Celery, shell), `get_or_set` / `memoize` just call the loader every time. Nothing sticks on the worker.
|
|
44
|
+
|
|
45
|
+
### Manual scopes
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
with stash.stash_scope():
|
|
49
|
+
...
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Useful for tests, management commands, or task bodies that want the same behaviour.
|
|
53
|
+
|
|
54
|
+
### Clearing
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
stash.clear("wagtail.site_root_paths") # one key
|
|
58
|
+
stash.clear() # everything in this scope
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Rules of thumb
|
|
62
|
+
|
|
63
|
+
1. **Scope owns lifetime** — middleware / `stash_scope()` enable at enter and disable in `finally`.
|
|
64
|
+
2. **No scope ⇒ no stash** — avoids sticky process caches on Gunicorn sync workers.
|
|
65
|
+
3. **Prefer immutable-ish values** — stash stores/returns shallow copies; don't stash live model graphs you plan to mutate in place.
|
|
66
|
+
4. **Shared cache is separate** — if you need cross-process invalidation, keep Django's cache as L2 and use stash as L1 in front of it.
|
|
67
|
+
|
|
68
|
+
## Development
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
cd django-stash
|
|
72
|
+
python -m venv .venv && source .venv/bin/activate
|
|
73
|
+
pip install -e ".[testing,development]"
|
|
74
|
+
python testmanage.py test
|
|
75
|
+
```
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "django-stash"
|
|
3
|
+
description = "Scoped ambient memoization for Django — stash values for the duration of a request or task, available anywhere in the stack."
|
|
4
|
+
authors = [{name = "Andy Babic", email = "andyjbabic@gmail.com"}]
|
|
5
|
+
maintainers = [
|
|
6
|
+
{name = "Andy Babic", email = "andyjbabic@gmail.com"},
|
|
7
|
+
]
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
license = {file = "LICENSE"}
|
|
10
|
+
keywords = ["Django", "cache", "memoize", "request", "asgiref", "performance"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 3 - Alpha",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"License :: OSI Approved :: BSD License",
|
|
15
|
+
"Operating System :: OS Independent",
|
|
16
|
+
"Programming Language :: Python",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Programming Language :: Python :: 3.13",
|
|
21
|
+
"Framework :: Django",
|
|
22
|
+
"Framework :: Django :: 4.2",
|
|
23
|
+
"Framework :: Django :: 5.0",
|
|
24
|
+
"Framework :: Django :: 5.1",
|
|
25
|
+
"Framework :: Django :: 5.2",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
dynamic = ["version"]
|
|
29
|
+
requires-python = ">=3.11"
|
|
30
|
+
dependencies = [
|
|
31
|
+
"Django>=4.2",
|
|
32
|
+
"asgiref>=3.7",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
testing = [
|
|
37
|
+
"coverage>=7.0,<8.0",
|
|
38
|
+
]
|
|
39
|
+
development = [
|
|
40
|
+
"ruff>=0.1.1,<1.0",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[project.urls]
|
|
44
|
+
Source = "https://github.com/ababic/django-stash"
|
|
45
|
+
Changelog = "https://github.com/ababic/django-stash/blob/main/CHANGELOG.md"
|
|
46
|
+
|
|
47
|
+
[build-system]
|
|
48
|
+
requires = ["flit_core >=3.2,<4"]
|
|
49
|
+
build-backend = "flit_core.buildapi"
|
|
50
|
+
|
|
51
|
+
[tool.flit.module]
|
|
52
|
+
name = "stash"
|
|
53
|
+
|
|
54
|
+
[tool.flit.sdist]
|
|
55
|
+
exclude = [
|
|
56
|
+
".*",
|
|
57
|
+
"*.db",
|
|
58
|
+
"*.json",
|
|
59
|
+
"*.ini",
|
|
60
|
+
"*.sqlite3",
|
|
61
|
+
"*.yaml",
|
|
62
|
+
"tests",
|
|
63
|
+
"CHANGELOG.md",
|
|
64
|
+
"ruff.toml",
|
|
65
|
+
"manage.py",
|
|
66
|
+
"testmanage.py",
|
|
67
|
+
]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from stash.__version__ import __version__
|
|
2
|
+
from stash.api import (
|
|
3
|
+
clear,
|
|
4
|
+
disable,
|
|
5
|
+
enable,
|
|
6
|
+
enabled,
|
|
7
|
+
get,
|
|
8
|
+
get_or_set,
|
|
9
|
+
memoize,
|
|
10
|
+
set,
|
|
11
|
+
stash_scope,
|
|
12
|
+
)
|
|
13
|
+
from stash.middleware import StashMiddleware
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"StashMiddleware",
|
|
18
|
+
"__version__",
|
|
19
|
+
"clear",
|
|
20
|
+
"disable",
|
|
21
|
+
"enable",
|
|
22
|
+
"enabled",
|
|
23
|
+
"get",
|
|
24
|
+
"get_or_set",
|
|
25
|
+
"memoize",
|
|
26
|
+
"set",
|
|
27
|
+
"stash_scope",
|
|
28
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable, Iterator
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
from functools import wraps
|
|
8
|
+
from typing import Any, TypeVar, overload
|
|
9
|
+
|
|
10
|
+
from asgiref.local import Local
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
T = TypeVar("T")
|
|
14
|
+
|
|
15
|
+
_local = Local()
|
|
16
|
+
_MISSING = object()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def enabled() -> bool:
|
|
20
|
+
"""Return whether a stash scope is active for this execution context."""
|
|
21
|
+
return bool(getattr(_local, "enabled", False))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def enable() -> None:
|
|
25
|
+
"""
|
|
26
|
+
Open a stash scope for this execution context.
|
|
27
|
+
|
|
28
|
+
Prefer ``StashMiddleware`` or ``stash_scope()`` so lifetime is bounded.
|
|
29
|
+
"""
|
|
30
|
+
_local.enabled = True
|
|
31
|
+
_local.values = {}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def disable() -> None:
|
|
35
|
+
"""Close the stash scope and drop all stored values for this context."""
|
|
36
|
+
_local.values = {}
|
|
37
|
+
_local.enabled = False
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _values() -> dict[str, Any]:
|
|
41
|
+
values = getattr(_local, "values", None)
|
|
42
|
+
if values is None:
|
|
43
|
+
values = {}
|
|
44
|
+
_local.values = values
|
|
45
|
+
return values
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _store(value: Any) -> Any:
|
|
49
|
+
return copy.copy(value)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _retrieve(value: Any) -> Any:
|
|
53
|
+
return copy.copy(value)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def get(key: str, default: Any = None) -> Any:
|
|
57
|
+
"""Return a stashed value, or ``default`` on miss / when no scope is active."""
|
|
58
|
+
if not enabled():
|
|
59
|
+
return default
|
|
60
|
+
value = _values().get(key, _MISSING)
|
|
61
|
+
if value is _MISSING:
|
|
62
|
+
return default
|
|
63
|
+
return _retrieve(value)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def set(key: str, value: Any) -> None:
|
|
67
|
+
"""
|
|
68
|
+
Store ``value`` under ``key`` when a scope is active.
|
|
69
|
+
|
|
70
|
+
No-op when no scope is active (avoids sticky process-level state).
|
|
71
|
+
"""
|
|
72
|
+
if not enabled():
|
|
73
|
+
return
|
|
74
|
+
_values()[key] = _store(value)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def clear(key: str | None = None) -> None:
|
|
78
|
+
"""Clear one key, or the entire stash when ``key`` is omitted."""
|
|
79
|
+
if not enabled():
|
|
80
|
+
return
|
|
81
|
+
if key is None:
|
|
82
|
+
_local.values = {}
|
|
83
|
+
return
|
|
84
|
+
_values().pop(key, None)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def get_or_set(key: str, loader: Callable[[], T]) -> T:
|
|
88
|
+
"""
|
|
89
|
+
Return a stashed value, or call ``loader``, stash the result, and return it.
|
|
90
|
+
|
|
91
|
+
When no scope is active, always calls ``loader()`` and does not store.
|
|
92
|
+
"""
|
|
93
|
+
if not enabled():
|
|
94
|
+
return loader()
|
|
95
|
+
|
|
96
|
+
values = _values()
|
|
97
|
+
value = values.get(key, _MISSING)
|
|
98
|
+
if value is not _MISSING:
|
|
99
|
+
return _retrieve(value)
|
|
100
|
+
|
|
101
|
+
loaded = loader()
|
|
102
|
+
values[key] = _store(loaded)
|
|
103
|
+
return _retrieve(loaded)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _memo_key(func: Callable[..., Any], args: tuple[Any, ...], kwargs: dict[str, Any]) -> str:
|
|
107
|
+
qualname = f"{func.__module__}.{func.__qualname__}"
|
|
108
|
+
if not args and not kwargs:
|
|
109
|
+
return qualname
|
|
110
|
+
return f"{qualname}:{args!r}:{sorted(kwargs.items())!r}"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@overload
|
|
114
|
+
def memoize(func: Callable[..., T]) -> Callable[..., T]: ...
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@overload
|
|
118
|
+
def memoize(
|
|
119
|
+
*,
|
|
120
|
+
key: str | None = None,
|
|
121
|
+
) -> Callable[[Callable[..., T]], Callable[..., T]]: ...
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def memoize(
|
|
125
|
+
func: Callable[..., T] | None = None,
|
|
126
|
+
*,
|
|
127
|
+
key: str | None = None,
|
|
128
|
+
) -> Callable[..., T] | Callable[[Callable[..., T]], Callable[..., T]]:
|
|
129
|
+
"""
|
|
130
|
+
Memoize a function into the active stash scope.
|
|
131
|
+
|
|
132
|
+
``@memoize`` derives a key from the function and call arguments.
|
|
133
|
+
``@memoize(key="...")`` uses a fixed key (argument values ignored).
|
|
134
|
+
Outside a scope, the function always runs normally.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
def decorator(fn: Callable[..., T]) -> Callable[..., T]:
|
|
138
|
+
@wraps(fn)
|
|
139
|
+
def wrapper(*args: Any, **kwargs: Any) -> T:
|
|
140
|
+
cache_key = key if key is not None else _memo_key(fn, args, kwargs)
|
|
141
|
+
return get_or_set(cache_key, lambda: fn(*args, **kwargs))
|
|
142
|
+
|
|
143
|
+
return wrapper
|
|
144
|
+
|
|
145
|
+
if func is not None:
|
|
146
|
+
return decorator(func)
|
|
147
|
+
return decorator
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@contextmanager
|
|
151
|
+
def stash_scope() -> Iterator[None]:
|
|
152
|
+
"""Context manager that opens a stash scope for a block of work."""
|
|
153
|
+
enable()
|
|
154
|
+
try:
|
|
155
|
+
yield
|
|
156
|
+
finally:
|
|
157
|
+
disable()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
|
|
5
|
+
from django.http import HttpRequest, HttpResponse
|
|
6
|
+
|
|
7
|
+
from stash.api import disable, enable
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class StashMiddleware:
|
|
11
|
+
"""
|
|
12
|
+
Open a stash scope for the duration of one HTTP request.
|
|
13
|
+
|
|
14
|
+
Outside this middleware (management commands, Celery tasks, etc.) stash
|
|
15
|
+
reads miss and writes no-op, so values cannot leak across units of work on
|
|
16
|
+
a reused worker thread.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, get_response: Callable[[HttpRequest], HttpResponse]) -> None:
|
|
20
|
+
self.get_response = get_response
|
|
21
|
+
|
|
22
|
+
def __call__(self, request: HttpRequest) -> HttpResponse:
|
|
23
|
+
enable()
|
|
24
|
+
try:
|
|
25
|
+
return self.get_response(request)
|
|
26
|
+
finally:
|
|
27
|
+
disable()
|