Cachalot 2.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.
cachalot-2.0.0/LICENSE ADDED
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2018, Radek Sprta
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,156 @@
1
+ Metadata-Version: 2.5
2
+ Name: Cachalot
3
+ Version: 2.0.0
4
+ Summary: Minimal persistent memoization cache
5
+ Project-URL: Homepage, https://radek-sprta.gitlab.io/Cachalot/
6
+ Project-URL: Repository, https://gitlab.com/radek-sprta/Cachalot
7
+ Project-URL: Documentation, https://radek-sprta.gitlab.io/Cachalot/
8
+ Author-email: Radek Sprta <mail@radeksprta.eu>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: cache,memoization
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Natural Language :: English
14
+ Classifier: Operating System :: OS Independent
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: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: Database
21
+ Classifier: Topic :: Software Development :: Libraries
22
+ Requires-Python: >=3.11
23
+ Requires-Dist: jsonpickle
24
+ Requires-Dist: tinydb>=4
25
+ Description-Content-Type: text/markdown
26
+
27
+ # Cachalot [![PyPI version](https://badge.fury.io/py/cachalot.svg)](https://badge.fury.io/py/cachalot) [![Pipeline status](https://gitlab.com/radek-sprta/cachalot/badges/master/pipeline.svg)](https://gitlab.com/radek-sprta/cachalot/commits/master) [![Coverage report](https://gitlab.com/radek-sprta/cachalot/badges/master/coverage.svg)](https://gitlab.com/radek-sprta/cachalot/commits/master)[![Downloads](http://pepy.tech/badge/cachalot)](http://pepy.tech/project/cachalot)
28
+
29
+ Cachalot is a minimal persistent memoization cache. It provides a decorator, that stores function result for future use. Perfect for heavy computations and I/O operation (such as web requests). On backend, it uses TinyDB for storage.
30
+
31
+ ## Features
32
+ - Simple usage via decorator
33
+ - Persistent caching
34
+ - Optional in-memory caching
35
+ - Key expiration
36
+ - Maximum cache size, to prevent bloat
37
+
38
+ ## Installation
39
+ Cachalot requires Python 3.11 or newer to run.
40
+
41
+ **Python package**
42
+
43
+ You can easily install Cachalot using pip:
44
+
45
+ `pip3 install cachalot`
46
+
47
+ **Manual**
48
+
49
+ Alternatively, to get the latest development version, you can clone this repository and then manually install it:
50
+
51
+ ```
52
+ git clone git@gitlab.com:radek-sprta/cachalot.git
53
+ cd cachalot
54
+ pip install .
55
+ ```
56
+
57
+ ## Usage
58
+ ```python
59
+ from cachalot import Cache
60
+
61
+
62
+ @Cache()
63
+ def expensive_function():
64
+ return expensive_calculation()
65
+ ```
66
+
67
+ ### Advanced usage
68
+ ```python
69
+ from cachalot import Cache
70
+
71
+
72
+ @Cache(
73
+ path="cache.json",
74
+ timeout=3600,
75
+ size=5000,
76
+ filesize=1_000_000,
77
+ retry=True,
78
+ renew_on_read=True,
79
+ )
80
+ def expensive_function():
81
+ return expensive_calculation()
82
+ ```
83
+
84
+ - `path`: Path to the database file. Defaults to .cache.json. If None, the cache is kept in memory.
85
+ - `timeout`: How long should the data be cached in seconds. Defaults to 0 (infinite).
86
+ - `size`: Maximum number of keys cached. Defaults to 0 (infinite).
87
+ - `filesize`: Maximum size of database file in bytes. Defaults to 0 (infinite). Cannot be used with an in-memory cache.
88
+ - `retry`: Retry if the cached result is None. Defaults to False.
89
+ - `renew_on_read`: Renew the entry, i.e refresh the entry timestamp on reads. Defaults to True.
90
+
91
+ ### Caveats
92
+ - Cache keys are the function's module and qualified name plus positional and keyword arguments (keyword order irrelevant). Defaults are not applied: `f(1)` and `f(1, b=2)` differ even if `2` is the default, and so do positional and keyword calls. For methods, `self` is intentionally excluded, so all instances share results.
93
+ - Functions with the same qualified name share a key: lambdas in the same scope, and functions made by the same factory (e.g. `make_scaler(2)` and `make_scaler(3)`). Closure state is not part of the key; use named top-level functions instead.
94
+ - Arguments are encoded with jsonpickle, which is not canonical: sets may encode in a different order between processes, causing misses.
95
+ - When `size` is exceeded, the least recently used entry is evicted. Use is the last read with `renew_on_read=True`, otherwise the last write.
96
+ - Results larger than `filesize` bytes are not cached (the entry is skipped, not stored).
97
+ - Cache objects on the same file within one process share one database. The file is still not locked; do not share it between concurrent processes or threads.
98
+ - Values are stored with jsonpickle; loading a cache file can execute code. Only use cache files from trusted sources.
99
+
100
+ ### Manually deleting entries
101
+ If you want to manually invalidate an entry, you can calculate the hash of the function call and then pass it to the `remove` method.
102
+
103
+ ```python
104
+ cache = Cache()
105
+
106
+
107
+ @cache
108
+ def expensive_function(arg):
109
+ return expensive_calculation(arg)
110
+
111
+
112
+ key = cache.calculate_hash(expensive_function)("arg")
113
+ cache.remove(key)
114
+ ```
115
+
116
+ For more information, see [documentation][documentation].
117
+
118
+ ## Contributing
119
+ For information on how to contribute to the project, please check the [Contributor's Guide][contributing]
120
+
121
+ ## Contact
122
+ [mail@radeksprta.eu](mailto:mail@radeksprta.eu)
123
+
124
+ [incoming+radek-sprta/cachalot@gitlab.com](mailto:incoming+radek-sprta/cachalot@gitlab.com)
125
+
126
+ ## License
127
+ MIT License
128
+
129
+ ## Credits
130
+ This package was created with [Cookiecutter][cookiecutter] and the [python-cookiecutter][python-cookiecutter] project template. Inspired by [Cashier][cachier]
131
+
132
+ ## Contributors ✨
133
+ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
134
+
135
+ <!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
136
+ <!-- prettier-ignore-start -->
137
+ <!-- markdownlint-disable -->
138
+ <table>
139
+ <tr>
140
+ <td align="center"><a href="https://gitlab.com/Evidlo"><img src="https://secure.gravatar.com/avatar/2670a8eba83e9233eb254fa48a12249f?s=80&d=identicon?s=100" width="100px;" alt=""/><br /><sub><b>Evan Widloski</b></sub></a><br /><a href="https://gitlab.com/radek-sprta/Cachalot/commits/master" title="Code">💻</a></td>
141
+ <td align="center"><a href="https://gitlab.com/sasa-tomic"><img src="https://assets.gitlab-static.net/uploads/-/system/user/avatar/4715393/avatar.png?s=100" width="100px;" alt=""/><br /><sub><b>Saša Tomić</b></sub></a><br /><a href="https://gitlab.com/radek-sprta/Cachalot/commits/master" title="Code">💻</a></td>
142
+ </tr>
143
+ </table>
144
+
145
+ <!-- markdownlint-restore -->
146
+ <!-- prettier-ignore-end -->
147
+
148
+ <!-- ALL-CONTRIBUTORS-LIST:END -->
149
+
150
+ This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
151
+
152
+ [cachier]: https://github.com/atmb4u/cashier
153
+ [contributing]: https://gitlab.com/radek-sprta/cachalot/blob/master/CONTRIBUTING.md
154
+ [cookiecutter]: https://github.com/audreyr/cookiecutter
155
+ [documentation]: https://radek-sprta.gitlab.io/cachalot
156
+ [python-cookiecutter]: https://gitlab.com/radek-sprta/python-cookiecutter
@@ -0,0 +1,130 @@
1
+ # Cachalot [![PyPI version](https://badge.fury.io/py/cachalot.svg)](https://badge.fury.io/py/cachalot) [![Pipeline status](https://gitlab.com/radek-sprta/cachalot/badges/master/pipeline.svg)](https://gitlab.com/radek-sprta/cachalot/commits/master) [![Coverage report](https://gitlab.com/radek-sprta/cachalot/badges/master/coverage.svg)](https://gitlab.com/radek-sprta/cachalot/commits/master)[![Downloads](http://pepy.tech/badge/cachalot)](http://pepy.tech/project/cachalot)
2
+
3
+ Cachalot is a minimal persistent memoization cache. It provides a decorator, that stores function result for future use. Perfect for heavy computations and I/O operation (such as web requests). On backend, it uses TinyDB for storage.
4
+
5
+ ## Features
6
+ - Simple usage via decorator
7
+ - Persistent caching
8
+ - Optional in-memory caching
9
+ - Key expiration
10
+ - Maximum cache size, to prevent bloat
11
+
12
+ ## Installation
13
+ Cachalot requires Python 3.11 or newer to run.
14
+
15
+ **Python package**
16
+
17
+ You can easily install Cachalot using pip:
18
+
19
+ `pip3 install cachalot`
20
+
21
+ **Manual**
22
+
23
+ Alternatively, to get the latest development version, you can clone this repository and then manually install it:
24
+
25
+ ```
26
+ git clone git@gitlab.com:radek-sprta/cachalot.git
27
+ cd cachalot
28
+ pip install .
29
+ ```
30
+
31
+ ## Usage
32
+ ```python
33
+ from cachalot import Cache
34
+
35
+
36
+ @Cache()
37
+ def expensive_function():
38
+ return expensive_calculation()
39
+ ```
40
+
41
+ ### Advanced usage
42
+ ```python
43
+ from cachalot import Cache
44
+
45
+
46
+ @Cache(
47
+ path="cache.json",
48
+ timeout=3600,
49
+ size=5000,
50
+ filesize=1_000_000,
51
+ retry=True,
52
+ renew_on_read=True,
53
+ )
54
+ def expensive_function():
55
+ return expensive_calculation()
56
+ ```
57
+
58
+ - `path`: Path to the database file. Defaults to .cache.json. If None, the cache is kept in memory.
59
+ - `timeout`: How long should the data be cached in seconds. Defaults to 0 (infinite).
60
+ - `size`: Maximum number of keys cached. Defaults to 0 (infinite).
61
+ - `filesize`: Maximum size of database file in bytes. Defaults to 0 (infinite). Cannot be used with an in-memory cache.
62
+ - `retry`: Retry if the cached result is None. Defaults to False.
63
+ - `renew_on_read`: Renew the entry, i.e refresh the entry timestamp on reads. Defaults to True.
64
+
65
+ ### Caveats
66
+ - Cache keys are the function's module and qualified name plus positional and keyword arguments (keyword order irrelevant). Defaults are not applied: `f(1)` and `f(1, b=2)` differ even if `2` is the default, and so do positional and keyword calls. For methods, `self` is intentionally excluded, so all instances share results.
67
+ - Functions with the same qualified name share a key: lambdas in the same scope, and functions made by the same factory (e.g. `make_scaler(2)` and `make_scaler(3)`). Closure state is not part of the key; use named top-level functions instead.
68
+ - Arguments are encoded with jsonpickle, which is not canonical: sets may encode in a different order between processes, causing misses.
69
+ - When `size` is exceeded, the least recently used entry is evicted. Use is the last read with `renew_on_read=True`, otherwise the last write.
70
+ - Results larger than `filesize` bytes are not cached (the entry is skipped, not stored).
71
+ - Cache objects on the same file within one process share one database. The file is still not locked; do not share it between concurrent processes or threads.
72
+ - Values are stored with jsonpickle; loading a cache file can execute code. Only use cache files from trusted sources.
73
+
74
+ ### Manually deleting entries
75
+ If you want to manually invalidate an entry, you can calculate the hash of the function call and then pass it to the `remove` method.
76
+
77
+ ```python
78
+ cache = Cache()
79
+
80
+
81
+ @cache
82
+ def expensive_function(arg):
83
+ return expensive_calculation(arg)
84
+
85
+
86
+ key = cache.calculate_hash(expensive_function)("arg")
87
+ cache.remove(key)
88
+ ```
89
+
90
+ For more information, see [documentation][documentation].
91
+
92
+ ## Contributing
93
+ For information on how to contribute to the project, please check the [Contributor's Guide][contributing]
94
+
95
+ ## Contact
96
+ [mail@radeksprta.eu](mailto:mail@radeksprta.eu)
97
+
98
+ [incoming+radek-sprta/cachalot@gitlab.com](mailto:incoming+radek-sprta/cachalot@gitlab.com)
99
+
100
+ ## License
101
+ MIT License
102
+
103
+ ## Credits
104
+ This package was created with [Cookiecutter][cookiecutter] and the [python-cookiecutter][python-cookiecutter] project template. Inspired by [Cashier][cachier]
105
+
106
+ ## Contributors ✨
107
+ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
108
+
109
+ <!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
110
+ <!-- prettier-ignore-start -->
111
+ <!-- markdownlint-disable -->
112
+ <table>
113
+ <tr>
114
+ <td align="center"><a href="https://gitlab.com/Evidlo"><img src="https://secure.gravatar.com/avatar/2670a8eba83e9233eb254fa48a12249f?s=80&d=identicon?s=100" width="100px;" alt=""/><br /><sub><b>Evan Widloski</b></sub></a><br /><a href="https://gitlab.com/radek-sprta/Cachalot/commits/master" title="Code">💻</a></td>
115
+ <td align="center"><a href="https://gitlab.com/sasa-tomic"><img src="https://assets.gitlab-static.net/uploads/-/system/user/avatar/4715393/avatar.png?s=100" width="100px;" alt=""/><br /><sub><b>Saša Tomić</b></sub></a><br /><a href="https://gitlab.com/radek-sprta/Cachalot/commits/master" title="Code">💻</a></td>
116
+ </tr>
117
+ </table>
118
+
119
+ <!-- markdownlint-restore -->
120
+ <!-- prettier-ignore-end -->
121
+
122
+ <!-- ALL-CONTRIBUTORS-LIST:END -->
123
+
124
+ This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
125
+
126
+ [cachier]: https://github.com/atmb4u/cashier
127
+ [contributing]: https://gitlab.com/radek-sprta/cachalot/blob/master/CONTRIBUTING.md
128
+ [cookiecutter]: https://github.com/audreyr/cookiecutter
129
+ [documentation]: https://radek-sprta.gitlab.io/cachalot
130
+ [python-cookiecutter]: https://gitlab.com/radek-sprta/python-cookiecutter
@@ -0,0 +1,50 @@
1
+ [project]
2
+ name = "Cachalot"
3
+ version = "2.0.0"
4
+ description = "Minimal persistent memoization cache"
5
+ authors = [{ name = "Radek Sprta", email = "mail@radeksprta.eu" }]
6
+ license = { text = "MIT" }
7
+ readme = "README.md"
8
+ requires-python = ">=3.11"
9
+ keywords = ["memoization", "cache"]
10
+ classifiers = [
11
+ "Development Status :: 5 - Production/Stable",
12
+ "Operating System :: OS Independent",
13
+ "Natural Language :: English",
14
+ "Topic :: Database",
15
+ "Topic :: Software Development :: Libraries",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Programming Language :: Python :: 3.13",
20
+ "Programming Language :: Python :: 3.14",
21
+ ]
22
+ dependencies = [
23
+ "tinydb>=4",
24
+ "jsonpickle",
25
+ ]
26
+
27
+ [project.urls]
28
+ Homepage = "https://radek-sprta.gitlab.io/Cachalot/"
29
+ Repository = "https://gitlab.com/radek-sprta/Cachalot"
30
+ Documentation = "https://radek-sprta.gitlab.io/Cachalot/"
31
+
32
+ [build-system]
33
+ requires = ["hatchling"]
34
+ build-backend = "hatchling.build"
35
+
36
+ [tool.hatch.build.targets.wheel]
37
+ packages = ["src/cachalot"]
38
+
39
+ [dependency-groups]
40
+ dev = [
41
+ "mkdocs>=1.6.1",
42
+ "mkdocs-cinder>=1.2.0",
43
+ "ruff>=0.16.10",
44
+ "pytest>=9.1.1",
45
+ "pytest-cov>=7.1.0",
46
+ ]
47
+
48
+ [tool.pytest.ini_options]
49
+ addopts = "--showlocals --verbose -r w --cov=cachalot --cov-report=term-missing"
50
+ norecursedirs = [".*", "build", "site", "dist"]
@@ -0,0 +1,312 @@
1
+ """Cachalot is a minimal persistent memoization cache, that uses TinyDB."""
2
+
3
+ import functools
4
+ import hashlib
5
+ import inspect
6
+ import json
7
+ import os
8
+ import pathlib
9
+ import time
10
+ import warnings
11
+ from collections.abc import Callable
12
+ from importlib import metadata
13
+ from typing import Any
14
+
15
+ import jsonpickle
16
+ import tinydb
17
+
18
+ try:
19
+ __version__ = metadata.version(__name__)
20
+ except metadata.PackageNotFoundError: # pragma: no cover
21
+ __version__ = "unknown"
22
+
23
+ # Not thread-safe
24
+ _file_databases: dict[str, tinydb.TinyDB] = {}
25
+
26
+
27
+ class Cache:
28
+ """Offline cache for search results.
29
+
30
+ Attributes:
31
+ path: Defaults to .cache.json. Path to the database file. If None, the
32
+ cache is kept in memory and the storage type is MemoryStorage.
33
+ timeout: Defaults to infinite. Period after which results should expire.
34
+ size: Defaults to infinite. Maximum number of cached results.
35
+ filesize: Defaults to infinite. Maximum size of database file in bytes.
36
+ Cannot be used with an in-memory cache.
37
+ retry: Defaults to False. Whether to retry when the cached result is None.
38
+ renew_on_read: Defaults to True. Whether to refresh timestamps on reads.
39
+ storage: Deprecated and ignored.
40
+ """
41
+
42
+ def __init__(
43
+ self,
44
+ *,
45
+ path: str | None = ".cache.json",
46
+ timeout: int = 0,
47
+ size: int = 0,
48
+ filesize: int = 0,
49
+ retry: bool = False,
50
+ renew_on_read: bool = True,
51
+ storage: Any = None,
52
+ ) -> None:
53
+ if path is None and filesize > 0:
54
+ raise ValueError("filesize cannot be used with an in-memory cache")
55
+ if storage is not None:
56
+ warnings.warn(
57
+ "storage is ignored and inferred from path",
58
+ DeprecationWarning,
59
+ stacklevel=2,
60
+ )
61
+ self.timeout = timeout
62
+ self.size = size
63
+ self.filesize = filesize
64
+ self.retry = retry
65
+ self.renew_on_read = renew_on_read
66
+ self.uncached = object() # Sentinel object to check uncached functions
67
+ self.path = (
68
+ os.path.abspath(os.path.expanduser(path)) if path is not None else None
69
+ )
70
+ if self.path is None:
71
+ self.storage = tinydb.storages.MemoryStorage
72
+ self._memory_db = tinydb.TinyDB(storage=self.storage)
73
+ else:
74
+ self.storage = tinydb.storages.JSONStorage
75
+ os.makedirs(os.path.dirname(self.path), exist_ok=True)
76
+ self._open_file_db()
77
+
78
+ @property
79
+ def db(self) -> tinydb.TinyDB:
80
+ """Database shared by all caches on the same file."""
81
+ if self.path is None:
82
+ return self._memory_db
83
+ if self.path not in _file_databases:
84
+ _file_databases[self.path] = tinydb.TinyDB(self.path, storage=self.storage)
85
+ return _file_databases[self.path]
86
+
87
+ def close(self) -> None:
88
+ """Close the database; other caches on the same file reopen it on next use."""
89
+ if self.path is None:
90
+ self._memory_db.close()
91
+ elif (database := _file_databases.pop(self.path, None)) is not None:
92
+ database.close()
93
+
94
+ def _open_file_db(self) -> None:
95
+ """Replace the shared database of the file, closing the previous one."""
96
+ if (previous := _file_databases.get(self.path)) is not None:
97
+ previous.close()
98
+ _file_databases[self.path] = tinydb.TinyDB(self.path, storage=self.storage)
99
+
100
+ @staticmethod
101
+ def _arguments(
102
+ function: Callable[..., Any], args: tuple[Any, ...], kwargs: dict[str, Any]
103
+ ) -> tuple[tuple[Any, ...], dict[str, Any]]:
104
+ """Return the call arguments that identify a call.
105
+
106
+ Keyword arguments are sorted by key and self is dropped for methods,
107
+ so that calls differing only in keyword order are equal. Defaults are
108
+ not applied, so passing a default explicitly yields a different key.
109
+
110
+ Args:
111
+ function: Function being called.
112
+ args: Positional arguments of the call.
113
+ kwargs: Keyword arguments of the call.
114
+
115
+ Returns:
116
+ Positional arguments and keyword arguments sorted by key.
117
+ """
118
+ if Cache._is_method(function):
119
+ args = args[1:]
120
+ return args, dict(sorted(kwargs.items()))
121
+
122
+ def calculate_hash(self, function: Callable[..., Any]) -> Callable[..., str]:
123
+ """Calculate hash of a function call.
124
+
125
+ Args:
126
+ function: Function to calculate hash of.
127
+
128
+ Returns:
129
+ Function taking the call arguments and returning their hash.
130
+
131
+ Example:
132
+ >>> cache.calculate_hash(len)('teststring')
133
+ 'a3d600dd2d9a535ca47674474ebbefec'
134
+ """
135
+
136
+ def wrapped(*args, **kwargs):
137
+ arguments = self._arguments(function, args, kwargs)
138
+ seed = f"{function.__module__}.{function.__qualname__}" + jsonpickle.encode(
139
+ arguments, keys=True
140
+ )
141
+ return hashlib.md5(seed.encode("utf8")).hexdigest()
142
+
143
+ return wrapped
144
+
145
+ def clear(self) -> None:
146
+ """Clear the cache."""
147
+ try:
148
+ self.db.truncate()
149
+ except json.decoder.JSONDecodeError:
150
+ # Database is corrupted
151
+ self._recreate_db()
152
+
153
+ def expiry(self) -> float:
154
+ """Return expiration time for cached results."""
155
+ return time.time() + self.timeout
156
+
157
+ def _get_with_sentinel(self, key: str) -> Any:
158
+ """Private get implementation, that returns self.uncached sentinel
159
+ object, if key is not cached in the database. Exists so we do not
160
+ break the API.
161
+
162
+ Args:
163
+ key: Key hash.
164
+
165
+ Returns:
166
+ Cached object or self.uncached.
167
+ """
168
+ self._remove_expired()
169
+ entry = tinydb.Query()
170
+ try:
171
+ value = self.db.get(entry.key == key)
172
+ if value:
173
+ if self.renew_on_read:
174
+ self.db.update(
175
+ {"time": self.expiry(), "used": time.time()},
176
+ entry.key == key,
177
+ )
178
+ return jsonpickle.decode(value["value"])
179
+ except json.decoder.JSONDecodeError:
180
+ # Database is corrupted
181
+ self._recreate_db()
182
+ return self.uncached
183
+
184
+ def get(self, key: str) -> Any:
185
+ """Get entry from database.
186
+
187
+ Args:
188
+ key: Key hash.
189
+
190
+ Returns:
191
+ Cached object.
192
+ """
193
+ value = self._get_with_sentinel(key)
194
+ return None if value is self.uncached else value
195
+
196
+ def insert(self, key: str, entry: Any) -> None:
197
+ """Insert entry into cache.
198
+
199
+ Args:
200
+ key: Key hash of the entry to store.
201
+ entry: Object to cache.
202
+ """
203
+ document = {
204
+ "key": key,
205
+ "time": self.expiry(),
206
+ "used": time.time(),
207
+ "value": jsonpickle.encode(entry),
208
+ }
209
+ if self.filesize > 0:
210
+ alone = json.dumps({"_default": {"1": document}}).encode()
211
+ if len(alone) > self.filesize:
212
+ self.remove(key)
213
+ return
214
+ try:
215
+ self.db.upsert(document, tinydb.Query().key == key)
216
+ while 0 < self.size < len(self.db):
217
+ self._remove_oldest()
218
+ if self.filesize > 0:
219
+ while os.stat(self.path).st_size > self.filesize and len(self.db) > 0:
220
+ self._remove_oldest()
221
+ except json.decoder.JSONDecodeError:
222
+ # Database is corrupted
223
+ self._recreate_db()
224
+ self.db.upsert(document, tinydb.Query().key == key)
225
+
226
+ @staticmethod
227
+ def _is_method(function: Callable[..., Any]) -> bool:
228
+ """Check if function is actually a method.
229
+
230
+ Args:
231
+ function: Function to check the state of.
232
+
233
+ Returns:
234
+ True if function is method, false otherwise.
235
+ """
236
+ if inspect.ismethod(function) or "." not in function.__qualname__:
237
+ return False
238
+ return inspect.getfullargspec(function).args[:1] == ["self"]
239
+
240
+ def remove(self, key: str) -> None:
241
+ """Delete key from cache.
242
+
243
+ Args:
244
+ key: Hash key to delete from the cache.
245
+ """
246
+ entry = tinydb.Query()
247
+ try:
248
+ self.db.remove(entry.key == key)
249
+ except json.decoder.JSONDecodeError:
250
+ # Database is corrupted
251
+ self._recreate_db()
252
+
253
+ def _remove_expired(self) -> None:
254
+ """Remove old entries."""
255
+ if self.timeout < 1:
256
+ return
257
+
258
+ entry = tinydb.Query()
259
+ now = time.time()
260
+ try:
261
+ self.db.remove(entry.time < now)
262
+ except json.decoder.JSONDecodeError:
263
+ # Database is corrupted
264
+ self._recreate_db()
265
+
266
+ def _recreate_db(self) -> None:
267
+ if self.path is None:
268
+ self._memory_db = tinydb.TinyDB(storage=self.storage)
269
+ return
270
+ self.close()
271
+ pathlib.Path(self.path).unlink(missing_ok=True)
272
+ self._open_file_db()
273
+
274
+ def _remove_oldest(self) -> None:
275
+ """Remove least recently used entry."""
276
+ try:
277
+ oldest = min(self.db.all(), key=lambda doc: doc.get("used", 0))["key"]
278
+ self.remove(oldest)
279
+ except json.decoder.JSONDecodeError:
280
+ # Database is corrupted
281
+ self._recreate_db()
282
+
283
+ def __len__(self) -> int:
284
+ """Return the length of cache."""
285
+ try:
286
+ return len(self.db)
287
+ except json.decoder.JSONDecodeError:
288
+ # Database is corrupted
289
+ self._recreate_db()
290
+ return 0
291
+
292
+ def __call__(self, function: Callable[..., Any]):
293
+ """Decorator for caching function results.
294
+
295
+ Args:
296
+ function: Function to decorate.
297
+
298
+ Returns:
299
+ Cached function.
300
+ """
301
+
302
+ @functools.wraps(function)
303
+ def wrapped(*args, **kwargs):
304
+ """Cache function."""
305
+ key = self.calculate_hash(function)(*args, **kwargs)
306
+ result = self._get_with_sentinel(key)
307
+ if result is self.uncached or (result is None and self.retry):
308
+ result = function(*args, **kwargs)
309
+ self.insert(key, result)
310
+ return result
311
+
312
+ return wrapped