logged-cache 0.0.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.
- logged_cache-0.0.0/.gitignore +218 -0
- logged_cache-0.0.0/CHANGELOG.md +14 -0
- logged_cache-0.0.0/CONTRIBUTING.md +39 -0
- logged_cache-0.0.0/LICENSE +21 -0
- logged_cache-0.0.0/PKG-INFO +389 -0
- logged_cache-0.0.0/README.md +349 -0
- logged_cache-0.0.0/SECURITY.md +6 -0
- logged_cache-0.0.0/docs/index.md +95 -0
- logged_cache-0.0.0/mkdocs.yml +7 -0
- logged_cache-0.0.0/pyproject.toml +124 -0
- logged_cache-0.0.0/src/logged_cache/__init__.py +50 -0
- logged_cache-0.0.0/src/logged_cache/core.py +826 -0
- logged_cache-0.0.0/src/logged_cache/decorators.py +424 -0
- logged_cache-0.0.0/src/logged_cache/py.typed +1 -0
- logged_cache-0.0.0/tests/test_logged_cache.py +463 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[codz]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py.cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
# Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# UV
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
uv.lock
|
|
102
|
+
|
|
103
|
+
# poetry
|
|
104
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
105
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
106
|
+
# commonly ignored for libraries.
|
|
107
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
108
|
+
# poetry.lock
|
|
109
|
+
# poetry.toml
|
|
110
|
+
|
|
111
|
+
# pdm
|
|
112
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
113
|
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
|
114
|
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
|
115
|
+
# pdm.lock
|
|
116
|
+
# pdm.toml
|
|
117
|
+
.pdm-python
|
|
118
|
+
.pdm-build/
|
|
119
|
+
|
|
120
|
+
# pixi
|
|
121
|
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
|
122
|
+
# pixi.lock
|
|
123
|
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
|
124
|
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
|
125
|
+
.pixi
|
|
126
|
+
|
|
127
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
128
|
+
__pypackages__/
|
|
129
|
+
|
|
130
|
+
# Celery stuff
|
|
131
|
+
celerybeat-schedule
|
|
132
|
+
celerybeat.pid
|
|
133
|
+
|
|
134
|
+
# Redis
|
|
135
|
+
*.rdb
|
|
136
|
+
*.aof
|
|
137
|
+
*.pid
|
|
138
|
+
|
|
139
|
+
# RabbitMQ
|
|
140
|
+
mnesia/
|
|
141
|
+
rabbitmq/
|
|
142
|
+
rabbitmq-data/
|
|
143
|
+
|
|
144
|
+
# ActiveMQ
|
|
145
|
+
activemq-data/
|
|
146
|
+
|
|
147
|
+
# SageMath parsed files
|
|
148
|
+
*.sage.py
|
|
149
|
+
|
|
150
|
+
# Environments
|
|
151
|
+
.env
|
|
152
|
+
.envrc
|
|
153
|
+
.venv
|
|
154
|
+
env/
|
|
155
|
+
venv/
|
|
156
|
+
ENV/
|
|
157
|
+
env.bak/
|
|
158
|
+
venv.bak/
|
|
159
|
+
|
|
160
|
+
# Spyder project settings
|
|
161
|
+
.spyderproject
|
|
162
|
+
.spyproject
|
|
163
|
+
|
|
164
|
+
# Rope project settings
|
|
165
|
+
.ropeproject
|
|
166
|
+
|
|
167
|
+
# mkdocs documentation
|
|
168
|
+
/site
|
|
169
|
+
|
|
170
|
+
# mypy
|
|
171
|
+
.mypy_cache/
|
|
172
|
+
.dmypy.json
|
|
173
|
+
dmypy.json
|
|
174
|
+
|
|
175
|
+
# Pyre type checker
|
|
176
|
+
.pyre/
|
|
177
|
+
|
|
178
|
+
# pytype static type analyzer
|
|
179
|
+
.pytype/
|
|
180
|
+
|
|
181
|
+
# Cython debug symbols
|
|
182
|
+
cython_debug/
|
|
183
|
+
|
|
184
|
+
# PyCharm
|
|
185
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
186
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
187
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
188
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
189
|
+
.idea/
|
|
190
|
+
|
|
191
|
+
# Abstra
|
|
192
|
+
# Abstra is an AI-powered process automation framework.
|
|
193
|
+
# Ignore directories containing user credentials, local state, and settings.
|
|
194
|
+
# Learn more at https://abstra.io/docs
|
|
195
|
+
.abstra/
|
|
196
|
+
|
|
197
|
+
# Visual Studio Code
|
|
198
|
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
|
199
|
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
|
200
|
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
|
201
|
+
# you could uncomment the following to ignore the entire vscode folder
|
|
202
|
+
.vscode/
|
|
203
|
+
# Temporary file for partial code execution
|
|
204
|
+
tempCodeRunnerFile.py
|
|
205
|
+
|
|
206
|
+
# Ruff stuff:
|
|
207
|
+
.ruff_cache/
|
|
208
|
+
|
|
209
|
+
# PyPI configuration file
|
|
210
|
+
.pypirc
|
|
211
|
+
|
|
212
|
+
# Marimo
|
|
213
|
+
marimo/_static/
|
|
214
|
+
marimo/_lsp/
|
|
215
|
+
__marimo__/
|
|
216
|
+
|
|
217
|
+
# Streamlit
|
|
218
|
+
.streamlit/secrets.toml
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
<!-- version list -->
|
|
4
|
+
|
|
5
|
+
## v0.0.0 (2026-05-23)
|
|
6
|
+
|
|
7
|
+
- Initial Release
|
|
8
|
+
|
|
9
|
+
## 0.1.0
|
|
10
|
+
|
|
11
|
+
- Initial package structure.
|
|
12
|
+
- Added `LoggedCache`, `LRUCacheManager`, `TTLCacheManager`, stats, hooks, and
|
|
13
|
+
decorator helpers.
|
|
14
|
+
- Added tests, documentation, and GitHub Actions workflows.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
## Setup
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
uv sync --extra dev --extra docs
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Without `uv`:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
python -m venv .venv
|
|
13
|
+
. .venv/bin/activate
|
|
14
|
+
python -m pip install -e ".[dev,docs]"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Checks
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
ruff check .
|
|
21
|
+
mypy
|
|
22
|
+
pytest
|
|
23
|
+
python -m build
|
|
24
|
+
twine check dist/*
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Releases
|
|
28
|
+
|
|
29
|
+
Releases are automated with Python Semantic Release and Conventional Commits.
|
|
30
|
+
Pushes to `main` are analyzed and a release is created only when commits require
|
|
31
|
+
one:
|
|
32
|
+
|
|
33
|
+
- `fix: ...` creates a patch release.
|
|
34
|
+
- `feat: ...` creates a minor release.
|
|
35
|
+
- `feat!: ...` or a `BREAKING CHANGE:` footer creates a major release.
|
|
36
|
+
|
|
37
|
+
Release commits are generated as `chore(release): <version> [skip ci]`.
|
|
38
|
+
|
|
39
|
+
Please keep changes focused and include tests for behavior changes.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 51n91n51nk1n
|
|
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,389 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: logged-cache
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: Small logged cachetools wrappers with cache managers, stats, and decorators.
|
|
5
|
+
Project-URL: Homepage, https://github.com/51n91n51nk1n/logged_cache
|
|
6
|
+
Project-URL: Documentation, https://github.com/51n91n51nk1n/logged_cache#readme
|
|
7
|
+
Project-URL: Issues, https://github.com/51n91n51nk1n/logged_cache/issues
|
|
8
|
+
Project-URL: Source, https://github.com/51n91n51nk1n/logged_cache
|
|
9
|
+
Author: 51n91n51nk1n
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: cache,cachetools,logging,lru,ttl
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: cachetools<7,>=5.3
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
26
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
27
|
+
Requires-Dist: packaging>=25; extra == 'dev'
|
|
28
|
+
Requires-Dist: pkginfo>=1.12; extra == 'dev'
|
|
29
|
+
Requires-Dist: pre-commit>=4.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: pylint>=3.3; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: python-semantic-release>=10.5; extra == 'dev'
|
|
34
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
35
|
+
Requires-Dist: twine>=5.0; extra == 'dev'
|
|
36
|
+
Requires-Dist: types-cachetools>=6.2; extra == 'dev'
|
|
37
|
+
Provides-Extra: docs
|
|
38
|
+
Requires-Dist: mkdocs>=1.6; extra == 'docs'
|
|
39
|
+
Description-Content-Type: text/markdown
|
|
40
|
+
|
|
41
|
+
# logged-cache
|
|
42
|
+
|
|
43
|
+
`logged-cache` is a small Python package around
|
|
44
|
+
[`cachetools`](https://cachetools.readthedocs.io/) for applications that want
|
|
45
|
+
cache hit/miss logs without rewriting their caching code.
|
|
46
|
+
|
|
47
|
+
It provides:
|
|
48
|
+
|
|
49
|
+
- `LoggedCache`, a mutable mapping wrapper that logs cache activity.
|
|
50
|
+
- `LRUCacheManager` and `TTLCacheManager`, with a shared lock for
|
|
51
|
+
`cachetools.cached`.
|
|
52
|
+
- `CacheStats`, lightweight counters for hits, misses, writes, deletes, and
|
|
53
|
+
clears.
|
|
54
|
+
- `cached` and `cachedmethod`, convenience decorators for functions and methods.
|
|
55
|
+
- Synchronous and asynchronous function caching with the same decorator.
|
|
56
|
+
- Hooks and key formatters for metrics, redaction, and structured logging.
|
|
57
|
+
|
|
58
|
+
## Installation
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pip install logged-cache
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
For local development with `uv`:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
uv sync --extra dev --extra docs
|
|
68
|
+
uv run pytest
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
If you do not use `uv`, the project is still standard PEP 621 Python packaging:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
python -m venv .venv
|
|
75
|
+
. .venv/bin/activate
|
|
76
|
+
python -m pip install -e ".[dev,docs]"
|
|
77
|
+
pytest
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Quick Start
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
import logging
|
|
84
|
+
|
|
85
|
+
from logged_cache import (
|
|
86
|
+
LRUCacheManager,
|
|
87
|
+
TTLCacheManager,
|
|
88
|
+
cached,
|
|
89
|
+
cachedmethod,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
logger = logging.getLogger("myapp.cache")
|
|
93
|
+
|
|
94
|
+
# Use LRU when you want a bounded cache.
|
|
95
|
+
users = LRUCacheManager(maxsize=256, logger=logger, name="users")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@cached(users)
|
|
99
|
+
def load_user(user_id: int) -> dict[str, int | str]:
|
|
100
|
+
return {"id": user_id, "name": "Ada"}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# Use TTL when values should expire.
|
|
104
|
+
tokens = TTLCacheManager(maxsize=1000, ttl=300, logger=logger, name="tokens")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@cached(tokens)
|
|
108
|
+
def fetch_token(account_id: str) -> str:
|
|
109
|
+
return f"token-for-{account_id}"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# Use cachedmethod for instance or class methods.
|
|
113
|
+
class ProfileService:
|
|
114
|
+
cache = LRUCacheManager(maxsize=256, logger=logger, name="profiles")
|
|
115
|
+
|
|
116
|
+
@cachedmethod(cache)
|
|
117
|
+
def load_profile(self, user_id: int) -> dict[str, int | str]:
|
|
118
|
+
return {"id": user_id, "name": "Ada"}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# The same decorators work with async functions and methods.
|
|
122
|
+
@cached(users)
|
|
123
|
+
async def load_user_async(user_id: int) -> dict[str, int | str]:
|
|
124
|
+
return {"id": user_id, "name": "Ada"}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
load_user(1) # miss, then set.
|
|
128
|
+
load_user(1) # hit.
|
|
129
|
+
fetch_token("main") # TTL miss, then set.
|
|
130
|
+
ProfileService().load_profile(1) # method cache.
|
|
131
|
+
|
|
132
|
+
print(users.stats.hits, users.stats.misses, users.stats.hit_rate)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
With a standard logging formatter, cache events look like this:
|
|
136
|
+
|
|
137
|
+
```text
|
|
138
|
+
DEBUG:myapp.cache:[users] Cache miss: function=load_user key=(1,)
|
|
139
|
+
DEBUG:myapp.cache:[users] Cache set: function=load_user key=(1,)
|
|
140
|
+
DEBUG:myapp.cache:[users] Cache hit: function=load_user key=(1,)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
If `logger` is not provided, `logging.getLogger()` is used. Each log record also
|
|
144
|
+
contains `cache_name`, `cache_function`, `cache_event`, and `cache_key`
|
|
145
|
+
attributes for structured logging. When a cache is used through `cached`,
|
|
146
|
+
`cachedmethod`, `async_cached`, or `async_cachedmethod`, log messages also show
|
|
147
|
+
the function or method name.
|
|
148
|
+
|
|
149
|
+
If `name` is omitted, decorators set a readable fallback name from the decorated
|
|
150
|
+
callable, such as `myapp.services.UserService.load_user`.
|
|
151
|
+
|
|
152
|
+
Keys are formatted defensively before they are written to logs. Very large keys
|
|
153
|
+
are truncated, and plain object instances are shown as readable class names
|
|
154
|
+
instead of memory-address-heavy default representations:
|
|
155
|
+
|
|
156
|
+
```text
|
|
157
|
+
DEBUG:myapp.cache:[users] Cache miss: key=<myapp.queries.UserQuery>
|
|
158
|
+
DEBUG:myapp.cache:[users] Cache miss: key=('xxxxxxxxxxxxxxxxxxxx...<truncated>)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## LRU Cache
|
|
162
|
+
|
|
163
|
+
Use `LRUCacheManager` when you want a fixed-size cache where the least recently
|
|
164
|
+
used entries are evicted first.
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
import logging
|
|
168
|
+
|
|
169
|
+
from logged_cache import LRUCacheManager, cached
|
|
170
|
+
|
|
171
|
+
logger = logging.getLogger("myapp.cache")
|
|
172
|
+
users = LRUCacheManager(maxsize=256, logger=logger, name="users")
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@cached(users)
|
|
176
|
+
def load_user(user_id: int) -> dict[str, int | str]:
|
|
177
|
+
return {"id": user_id, "name": "Ada"}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
load_user(1)
|
|
181
|
+
load_user(1)
|
|
182
|
+
|
|
183
|
+
print(users.stats.hits, users.stats.misses, users.stats.hit_rate)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## TTL Cache
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
from logged_cache import TTLCacheManager, cached
|
|
190
|
+
|
|
191
|
+
tokens = TTLCacheManager(maxsize=1000, ttl=300)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@cached(tokens)
|
|
195
|
+
def fetch_token(account_id: str) -> str:
|
|
196
|
+
return f"token-for-{account_id}"
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Async Functions
|
|
200
|
+
|
|
201
|
+
Use the same `cached` decorator for `async def`. The awaited result is cached,
|
|
202
|
+
not the coroutine object.
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
from logged_cache import LRUCacheManager, cached
|
|
206
|
+
|
|
207
|
+
profiles = LRUCacheManager(maxsize=256)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@cached(profiles)
|
|
211
|
+
async def fetch_profile(user_id: int) -> dict[str, int | str]:
|
|
212
|
+
return {"id": user_id, "name": "Ada"}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
profile = await fetch_profile(1)
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
For async-only applications, use `AsyncLRUCacheManager` or
|
|
219
|
+
`AsyncTTLCacheManager` with `async_cached` to protect cache operations with
|
|
220
|
+
`asyncio.Lock`.
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
from logged_cache import AsyncLRUCacheManager, async_cached
|
|
224
|
+
|
|
225
|
+
profiles = AsyncLRUCacheManager(maxsize=256, name="profiles")
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
@async_cached(profiles)
|
|
229
|
+
async def fetch_profile(user_id: int) -> dict[str, int | str]:
|
|
230
|
+
return {"id": user_id, "name": "Ada"}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## Methods
|
|
234
|
+
|
|
235
|
+
Use `cachedmethod` for instance methods and class methods. By default, it uses
|
|
236
|
+
`cachetools.keys.methodkey`, so the first method argument (`self` or `cls`) is
|
|
237
|
+
not included in the cache key.
|
|
238
|
+
|
|
239
|
+
```python
|
|
240
|
+
from logged_cache import LRUCacheManager, cachedmethod
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class ProfileService:
|
|
244
|
+
cache = LRUCacheManager(maxsize=256)
|
|
245
|
+
|
|
246
|
+
@cachedmethod(cache)
|
|
247
|
+
def load_profile(self, user_id: int) -> dict[str, int | str]:
|
|
248
|
+
return {"id": user_id, "name": "Ada"}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Async methods work the same way:
|
|
252
|
+
|
|
253
|
+
```python
|
|
254
|
+
from logged_cache import AsyncLRUCacheManager, async_cachedmethod
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class AsyncProfileService:
|
|
258
|
+
cache = AsyncLRUCacheManager(maxsize=256)
|
|
259
|
+
|
|
260
|
+
@async_cachedmethod(cache)
|
|
261
|
+
async def load_profile(self, user_id: int) -> dict[str, int | str]:
|
|
262
|
+
return {"id": user_id, "name": "Ada"}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## Direct Mapping Usage
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
from cachetools import LRUCache
|
|
269
|
+
from logged_cache import LoggedCache
|
|
270
|
+
|
|
271
|
+
cache = LoggedCache(LRUCache(maxsize=2))
|
|
272
|
+
cache["a"] = 1
|
|
273
|
+
|
|
274
|
+
if "a" in cache:
|
|
275
|
+
print(cache["a"])
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Direct mapping operations emit the same event names:
|
|
279
|
+
|
|
280
|
+
```text
|
|
281
|
+
DEBUG:myapp.cache:[logged-cache] Cache set: key='a'
|
|
282
|
+
DEBUG:myapp.cache:[logged-cache] Cache hit: key='a'
|
|
283
|
+
DEBUG:myapp.cache:[logged-cache] Cache delete: key='a'
|
|
284
|
+
DEBUG:myapp.cache:[logged-cache] Cache cleared
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
## Redacting Sensitive Keys
|
|
288
|
+
|
|
289
|
+
By default, keys are logged with a safe formatter that truncates large values
|
|
290
|
+
and describes plain object instances by class. For user IDs, tokens, emails, or
|
|
291
|
+
other sensitive values, pass a formatter:
|
|
292
|
+
|
|
293
|
+
```python
|
|
294
|
+
from logged_cache import LRUCacheManager
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def redact_key(key: object) -> str:
|
|
298
|
+
return "<redacted>"
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
cache = LRUCacheManager(maxsize=128, key_formatter=redact_key)
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
To keep the default behavior but change the maximum key length, use
|
|
305
|
+
`format_cache_key`:
|
|
306
|
+
|
|
307
|
+
```python
|
|
308
|
+
from functools import partial
|
|
309
|
+
from logged_cache import LRUCacheManager, format_cache_key
|
|
310
|
+
|
|
311
|
+
cache = LRUCacheManager(
|
|
312
|
+
maxsize=128,
|
|
313
|
+
key_formatter=partial(format_cache_key, max_length=80),
|
|
314
|
+
)
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
## Naming Caches
|
|
318
|
+
|
|
319
|
+
Pass `name` when the same logger receives events from several caches.
|
|
320
|
+
|
|
321
|
+
```python
|
|
322
|
+
users = LRUCacheManager(maxsize=256, name="users")
|
|
323
|
+
tokens = TTLCacheManager(maxsize=1000, ttl=300, name="tokens")
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
If `name` is omitted and the manager is used with `cached` or `cachedmethod`,
|
|
327
|
+
the package sets a fallback from the decorated callable's module and qualified
|
|
328
|
+
name. Explicit names are never replaced by decorators.
|
|
329
|
+
|
|
330
|
+
## Exporting Metrics
|
|
331
|
+
|
|
332
|
+
Use `on_event` to bridge cache events into your metrics stack.
|
|
333
|
+
|
|
334
|
+
```python
|
|
335
|
+
from logged_cache import CacheEvent, LRUCacheManager
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def record_metric(event: CacheEvent, key: object) -> None:
|
|
339
|
+
print(f"cache.{event}")
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
cache = LRUCacheManager(maxsize=128, on_event=record_metric)
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
## API Overview
|
|
346
|
+
|
|
347
|
+
`LoggedCache(inner, logger=None, log_level=logging.DEBUG, stats=None,
|
|
348
|
+
key_formatter=repr, on_event=None, name=None)` wraps any mutable mapping, including
|
|
349
|
+
`cachetools` caches. If `logger` is `None`, the root logger from
|
|
350
|
+
`logging.getLogger()` is used.
|
|
351
|
+
|
|
352
|
+
`LRUCacheManager(maxsize, ...)` creates an `LRUCache`, a `LoggedCache`, an
|
|
353
|
+
`RLock`, and a shared `CacheStats` object.
|
|
354
|
+
|
|
355
|
+
`TTLCacheManager(maxsize, ttl, ...)` does the same for `TTLCache`.
|
|
356
|
+
|
|
357
|
+
`AsyncLRUCacheManager(maxsize, ...)` and `AsyncTTLCacheManager(maxsize, ttl, ...)`
|
|
358
|
+
use `asyncio.Lock` for async-only applications.
|
|
359
|
+
|
|
360
|
+
`cached(manager, key=cachetools.keys.hashkey, info=False)` returns a decorator
|
|
361
|
+
compatible with regular and async functions.
|
|
362
|
+
|
|
363
|
+
`cachedmethod(manager, key=cachetools.keys.methodkey, info=False)` returns a
|
|
364
|
+
decorator compatible with regular and async methods.
|
|
365
|
+
|
|
366
|
+
`async_cached(...)` and `async_cachedmethod(...)` are async-only variants for
|
|
367
|
+
async cache managers.
|
|
368
|
+
|
|
369
|
+
## Development
|
|
370
|
+
|
|
371
|
+
```bash
|
|
372
|
+
uv sync --extra dev --extra docs
|
|
373
|
+
uv run ruff check .
|
|
374
|
+
uv run pylint src/logged_cache tests
|
|
375
|
+
uv run mypy
|
|
376
|
+
uv run pytest
|
|
377
|
+
uv run python -m build
|
|
378
|
+
uv run twine check dist/*
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
Install pre-commit hooks with:
|
|
382
|
+
|
|
383
|
+
```bash
|
|
384
|
+
uv run pre-commit install
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
## License
|
|
388
|
+
|
|
389
|
+
MIT
|