onecache 0.7.0__py3-none-any.whl

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.
onecache/__init__.py ADDED
@@ -0,0 +1,241 @@
1
+ """Onecache main module."""
2
+
3
+ from collections import OrderedDict
4
+ from contextlib import contextmanager
5
+ from datetime import timedelta
6
+ from sys import getsizeof
7
+ from threading import Lock
8
+ from typing import Any, Dict, Optional
9
+
10
+ from onecache.cache_value import CacheValue
11
+ from onecache.utils import IS_PYPY, utcnow
12
+
13
+
14
+ class ExpirableCache(object):
15
+ """Class used for custom cache decorator, dummy expirable cache based on dict structure.
16
+
17
+ Params:
18
+ * **size (int)**: max items in dict. default=512
19
+ * **timeout (int)**: Timeout in milliseconds, if it is None,
20
+ there is no timeout. default=None
21
+ * **refresh_ttl (int)**: Refresh ttl anytime key is accessed. default=False
22
+ * **thread_safe (bool)**: Tell cache decorator to be thread safe. default=False
23
+ * **max_mem_size (int)**: max mem size inside cache structure. default=None which means no limit. For pypy this value is ignored as the objects can change by the JIT compilation.
24
+ """
25
+
26
+ def __init__(
27
+ self,
28
+ size: int = 512,
29
+ timeout: Optional[int] = None,
30
+ refresh_ttl=False,
31
+ thread_safe=False,
32
+ max_mem_size: Optional[int] = None,
33
+ ):
34
+ self.cache: Dict[str, CacheValue] = OrderedDict()
35
+ self.timeout = timeout
36
+ self.size = size
37
+ self.max_mem_size = None if IS_PYPY else max_mem_size
38
+ self.current_size = 0
39
+ self.refresh_ttl = refresh_ttl
40
+ self.lock = None
41
+ if thread_safe:
42
+ self.lock = Lock()
43
+
44
+ def set(self, key, data):
45
+ key_size = None
46
+ value = None
47
+ with self._scoped():
48
+ if len(self.cache) + 1 > self.size and key not in self.cache:
49
+ self._pop_one()
50
+
51
+ if self.timeout:
52
+ expire_at = utcnow() + timedelta(milliseconds=self.timeout)
53
+ value = CacheValue(data, expire_at)
54
+ else:
55
+ value = CacheValue(data)
56
+
57
+ if self.max_mem_size:
58
+ key_size = getsizeof(key)
59
+ # pop data if it will exceed max mem size
60
+ while (
61
+ self.current_size + key_size + value.size > self.max_mem_size
62
+ and len(self.cache) > 1
63
+ ):
64
+ self._pop_one()
65
+ self.cache[key] = value
66
+
67
+ if self.max_mem_size:
68
+ self.current_size += key_size + value.size
69
+
70
+ def _pop_one(self):
71
+ key = next(iter(self.cache))
72
+ self._remove_key(key)
73
+
74
+ def get(self, key, fetch_value=True):
75
+ self._check_expired(key)
76
+ cache_value = self.cache.get(key)
77
+ if cache_value:
78
+ if self.refresh_ttl:
79
+ with self._scoped():
80
+ self.refresh_key_ttl(key)
81
+ if fetch_value:
82
+ return cache_value.value
83
+ return cache_value
84
+
85
+ def _check_expired(self, key):
86
+ to_rm = []
87
+ if self.timeout:
88
+ for key, cache_value in self.cache.items():
89
+ if cache_value.expired():
90
+ to_rm.append(key)
91
+ for key in to_rm:
92
+ self._remove_key(key)
93
+
94
+ def _remove_key(self, key):
95
+ item = self.cache[key]
96
+ if self.max_mem_size:
97
+ key_size = getsizeof(key)
98
+ self.current_size -= item.size + key_size
99
+
100
+ del self.cache[key]
101
+
102
+ def __contains__(self, key):
103
+ self._check_expired(key)
104
+ return key in self.cache
105
+
106
+ def refresh_key_ttl(self, key: Any, milliseconds=None):
107
+ """Do refresh of key ttl, if present."""
108
+ cache_value = self.cache.get(key)
109
+ if cache_value and (milliseconds or self.timeout):
110
+ ms = milliseconds if milliseconds else self.timeout
111
+ expire_at = utcnow() + timedelta(milliseconds=ms)
112
+ cache_value.refresh_ttl(expire_at)
113
+
114
+ @classmethod
115
+ def get_key(cls, *args, **kwargs):
116
+ """Helper method to generate keys from *args, **kwargs."""
117
+ sorted_kwargs = sorted(kwargs.items(), key=lambda item: item[0])
118
+ kwargs_list = [f"{key}={cls.serialize_key(val)}" for key, val in sorted_kwargs]
119
+ items = list(args) + kwargs_list
120
+ return "-".join([cls.serialize_key(item) for item in items])
121
+
122
+ @classmethod
123
+ def serialize_key(cls, data: Any):
124
+ if isinstance(data, dict):
125
+ return ",".join(
126
+ [f"{key}={cls.serialize_key(val)}" for key, val in data.items()]
127
+ )
128
+ elif isinstance(data, list):
129
+ return ",".join([cls.serialize_key(val) for val in data])
130
+ elif isinstance(data, str):
131
+ return data
132
+ else:
133
+ return str(data)
134
+
135
+ def expire(self, *args, **kwargs):
136
+ """Usefull to expire any combination of *args, **kwargs."""
137
+ key = self.get_key(*args, **kwargs)
138
+ if key in self.cache:
139
+ self._remove_key(key)
140
+
141
+ @contextmanager
142
+ def _scoped(self):
143
+ if self.lock:
144
+ with self.lock:
145
+ yield
146
+ else:
147
+ yield
148
+
149
+
150
+ class LRUCache(ExpirableCache):
151
+ def get(self, key):
152
+ cache_value = super().get(key, fetch_value=False)
153
+ if cache_value:
154
+ with self._scoped():
155
+ self._increment(cache_value)
156
+ return cache_value.value
157
+
158
+ return None
159
+
160
+ def _increment(self, val: CacheValue):
161
+ val.increment()
162
+
163
+ def _pop_one(self):
164
+ ordered_cache = sorted(self.cache.items(), key=lambda item: item[1].access)
165
+ key, _ = ordered_cache[0]
166
+ self._remove_key(key)
167
+
168
+ @property
169
+ def access(self):
170
+ """Get accesses as map."""
171
+ return {key: val.access for key, val in self.cache.items()}
172
+
173
+
174
+ # Decorator!
175
+ class CacheDecorator:
176
+ """Decorator for ExpirableCache"""
177
+
178
+ def __init__(
179
+ self,
180
+ maxsize=512,
181
+ ttl: Optional[int] = None,
182
+ skip_args: bool = False,
183
+ cache_class=LRUCache,
184
+ refresh_ttl: Optional[bool] = False,
185
+ thread_safe: Optional[bool] = False,
186
+ max_mem_size: Optional[int] = None,
187
+ ):
188
+ """
189
+ Args:
190
+ * **maxsize (int)**: Maximun number of items to be cached. default: 512
191
+ * **ttl (int)**: time to expire in milliseconds, if None, it does not expire. default: None
192
+ * **skip_args (bool)**: apply cache as the function doesn't have any arguments, default: False
193
+ * **cache_class (class)**: Class to use for cache instance. default: LRUCache
194
+ * **refresh_ttl (bool)**: if cache with ttl, This flag makes key expiration timestamp to be
195
+ refresh per access. default: False
196
+ * **thread_safe (bool)**: tell decorator to use thread safe lock. default=False
197
+ * **max_mem_size (int)**: max mem size inside cache structure. default=None which means no limit. For pypy this value is ignored as the objects can change by the JIT compilation.
198
+ """
199
+ self.cache = cache_class(
200
+ maxsize,
201
+ ttl,
202
+ refresh_ttl=refresh_ttl,
203
+ thread_safe=thread_safe,
204
+ max_mem_size=max_mem_size,
205
+ )
206
+ self.maxsize = maxsize
207
+ self.skip_args = skip_args
208
+
209
+ def __call__(self, func):
210
+ def wrapper(*args, **kwargs):
211
+ key = "any" if self.skip_args else self.cache.get_key(*args, **kwargs)
212
+
213
+ if key not in self.cache:
214
+ resp = func(*args, **kwargs)
215
+ self.cache.set(key, resp)
216
+ return resp
217
+ else:
218
+ return self.cache.get(key)
219
+
220
+ wrapper.cache = self.cache
221
+
222
+ return wrapper
223
+
224
+
225
+ class AsyncCacheDecorator(CacheDecorator):
226
+ """Async Decorator for ExpirableCache"""
227
+
228
+ def __call__(self, func):
229
+ async def async_wrapper(*args, **kwargs):
230
+ key = "any" if self.skip_args else self.cache.get_key(*args, **kwargs)
231
+
232
+ if key not in self.cache:
233
+ resp = await func(*args, **kwargs)
234
+ self.cache.set(key, resp)
235
+ return resp
236
+ else:
237
+ return self.cache.get(key)
238
+
239
+ async_wrapper.cache = self.cache
240
+
241
+ return async_wrapper
@@ -0,0 +1,34 @@
1
+ from datetime import datetime
2
+ from sys import getsizeof
3
+ from typing import Any, Optional
4
+
5
+ from onecache.utils import IS_PYPY, utcnow
6
+
7
+
8
+ class CacheValue:
9
+ """Dummy class for handling cache values."""
10
+
11
+ def __init__(self, value: Any, expire_at: Optional[datetime] = None):
12
+ self.size = None if IS_PYPY else getsizeof(value)
13
+ self.access = 0
14
+
15
+ self.value = value
16
+ self.expire_at = expire_at
17
+
18
+ def set_expired(self, data):
19
+ self.expire_at = data
20
+
21
+ def expired(self):
22
+ """Check if value is expired."""
23
+ if self.expire_at:
24
+ return utcnow() > self.expire_at
25
+ return False # pragma: no cover
26
+
27
+ def refresh_ttl(self, expire_at: datetime):
28
+ self.expire_at = expire_at
29
+
30
+ def increment(self):
31
+ self.access += 1
32
+
33
+ def __eq__(self, otherinstance: "CacheValue"):
34
+ return self.value == otherinstance.value
onecache/utils.py ADDED
@@ -0,0 +1,11 @@
1
+ import datetime
2
+ import platform
3
+ import sys
4
+
5
+ IS_PYPY = platform.python_implementation().lower().startswith("pypy")
6
+
7
+ def utcnow():
8
+ if sys.version_info >= (3, 11):
9
+ return datetime.datetime.now(datetime.UTC)
10
+ else:
11
+ return datetime.datetime.utcnow()
@@ -0,0 +1,102 @@
1
+ Metadata-Version: 2.1
2
+ Name: onecache
3
+ Version: 0.7.0
4
+ Summary: Python cache for sync and async code
5
+ License: MIT
6
+ Author: Johanderson Mogollon
7
+ Author-email: johander1822@gmail.com
8
+ Requires-Python: >=3.8,<3.13
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.8
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
20
+ Description-Content-Type: text/markdown
21
+
22
+
23
+ [![Coverage Status](https://coveralls.io/repos/github/sonic182/onecache/badge.svg?branch=master)](https://coveralls.io/github/sonic182/onecache?branch=master)
24
+ ![github status](https://github.com/sonic182/onecache/actions/workflows/python.yml/badge.svg)
25
+ # OneCache
26
+
27
+ Python cache for sync and async code.
28
+
29
+ Cache uses LRU algoritm. Cache can optionally have TTL.
30
+
31
+ Tested in python 3.7, 3.9, 3.11 and pypy3.9 for windows, mac and linux (see github status badge), it should work in versions between them. It may work for python3.6
32
+
33
+ # Usage
34
+
35
+ ```python
36
+ from onecache import CacheDecorator
37
+ from onecache import AsyncCacheDecorator
38
+
39
+
40
+ class Counter:
41
+ def __init__(self, count=0):
42
+ self.count = count
43
+
44
+
45
+ @pytest.mark.asyncio
46
+ async def test_async_cache_counter():
47
+ """Test async cache, counter case."""
48
+ counter = Counter()
49
+
50
+ @AsyncCacheDecorator()
51
+ async def mycoro(counter: Counter):
52
+ counter.count += 1
53
+ return counter.count
54
+
55
+ assert 1 == (await mycoro(counter))
56
+ assert 1 == (await mycoro(counter))
57
+
58
+
59
+ def test_cache_counter():
60
+ """Test async cache, counter case."""
61
+ counter = Counter()
62
+
63
+ @CacheDecorator()
64
+ def sample(counter: Counter):
65
+ counter.count += 1
66
+ return counter.count
67
+
68
+ assert 1 == (sample(counter))
69
+ assert 1 == (sample(counter))
70
+ ```
71
+
72
+ Decorator classes supports the following arguments
73
+
74
+ * **maxsize (int)**: Maximun number of items to be cached. default: 512
75
+ * **ttl (int)**: time to expire in milliseconds, if None, it does not expire. default: None
76
+ * **skip_args (bool)**: apply cache as the function doesn't have any arguments, default: False
77
+ * **cache_class (class)**: Class to use for cache instance. default: LRUCache
78
+ * **refresh_ttl (bool)**: if cache with ttl, This flag makes key expiration timestamp to be refresh per access. default: False
79
+ * **thread_safe (bool)**: tell decorator to use thread safe lock. default=False
80
+ * **max_mem_size (int)**: max mem size in bytes. Ceil for sum of cache values sizes. default=None which means no limit. For pypy this value is ignored as the objects can change by the JIT compilation.
81
+
82
+ If num of records exceds maxsize, it drops the oldest.
83
+
84
+
85
+ # Development
86
+
87
+ Install packages with pip-tools:
88
+ ```bash
89
+ pip install pip-tools
90
+ pip-compile
91
+ pip-compile test-requirements.in
92
+ pip-sync requirements.txt test-requirements.txt
93
+ ```
94
+
95
+ # Contribute
96
+
97
+ 1. Fork
98
+ 2. create a branch `feature/your_feature`
99
+ 3. commit - push - pull request
100
+
101
+ Thanks :)
102
+
@@ -0,0 +1,6 @@
1
+ onecache/__init__.py,sha256=jwfOOuz_wmy9f1iXhIh4XZB4T-602qJOW3ny8TYXTqc,8148
2
+ onecache/cache_value.py,sha256=6IRKg_Pj6kYHcTJMqD0ua9FTs5LBMK0J2XUQogNlk8Q,900
3
+ onecache/utils.py,sha256=E8Sv5-QSS_gdnaFXIN4dIlR4FUqKKURJ64SkhPinVdU,268
4
+ onecache-0.7.0.dist-info/METADATA,sha256=Soyi6rNUTsccCJ_ZCclggCBSASsPTcLYxLw9AyaqY5k,3268
5
+ onecache-0.7.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
6
+ onecache-0.7.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 1.9.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any