onecache 0.8.1__tar.gz → 0.9.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,144 @@
1
+ # Changelog
2
+ All notable changes to this project will be documented in this file.
3
+
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.9.0] 2026-10-12
10
+ ### Added
11
+ - Cython implementation of the cache core, used automatically on CPython; pure Python fallback for PyPy and platforms without a prebuilt wheel
12
+ - `onecache.COMPILED` flag and `ONECACHE_NO_EXTENSIONS` environment variable to force the pure Python implementation
13
+ - Prebuilt wheels for Linux (manylinux/musllinux), macOS and Windows on x86_64 and ARM64, built with cibuildwheel
14
+ - Decorated functions keep the wrapped function metadata (`__name__`, `__doc__`, `__wrapped__`)
15
+ - `ExpirableCache.clear()` to drop every entry
16
+ - `onecache.monotonic()`, the clock used for `CacheValue.expire_at`
17
+ - Decorated functions support weak references and pickle by reference with both implementations
18
+
19
+ ### Changed
20
+ - `LRUCache` now evicts the least recently used entry (previously the least frequently accessed)
21
+ - Expiration checks are O(1) amortized instead of scanning the whole cache on every access, and eviction no longer sorts the cache
22
+ - `thread_safe=True` now guards every cache operation, not only writes, with a reentrant lock
23
+ - `max_mem_size` evicts entries until the new value fits, instead of always keeping at least one previous entry; a value bigger than `max_mem_size` is not cached
24
+ - Setting an existing key again moves it to the newest position in the eviction order
25
+ - `ExpirableCache.cache` is a read-only view; use `expire()` or `clear()` to remove entries. Its iteration order is not the eviction order
26
+ - `timeout`, `max_mem_size`, `current_size` and `thread_safe` are read-only attributes; the `lock` attribute was removed
27
+ - `CacheValue.expire_at` is a timestamp in seconds from `onecache.monotonic()` (`float("inf")` when it does not expire) instead of a `datetime`; that clock is not always `time.monotonic()`
28
+ - `CacheValue.size` is only computed when `max_mem_size` is set (0 otherwise)
29
+ - A `timeout` of 0 or lower means entries do not expire
30
+ - A cache size (`maxsize`) lower than 1 raises `ValueError`
31
+ - Eviction is no longer customizable by overriding `_pop_one()`
32
+ - Cache instances (`ExpirableCache`, `LRUCache`) can't be pickled or copied; `CacheValue` entries pickle only their own data
33
+ - On CPython, decorated functions are `CachedFunction` objects instead of plain functions (`inspect.isfunction()` is False), and compiled cache instances don't accept arbitrary attributes or weak references
34
+ - Build backend switched from poetry-core to setuptools + Cython
35
+
36
+ ### Removed
37
+ - `CacheValue.set_expired()`, `CacheValue.refresh_ttl()`, `CacheValue.increment()` and the `fetch_value` argument of `ExpirableCache.get()`
38
+ - `onecache.IS_PYPY` and `onecache.utcnow` re-exports (still available in `onecache.utils`)
39
+
40
+ ## [0.8.1] 2026-02-20
41
+ ### Changed
42
+ - Removed the maximum Python version constraint from runtime requirements (`python = ">=3.8"`)
43
+ - Updated Python classifiers and refreshed CI/docs for newer Python versions
44
+
45
+ ## [0.8.0] 2025-11-12
46
+ ### Removed
47
+ - Dropped the unused `packaging` dependency from runtime requirements
48
+
49
+ ## [0.7.2] 2025-10-30
50
+ ### Added
51
+ * python 3.14 compatibility
52
+
53
+ ## [0.7.1] 2025-01-26
54
+ ### Added
55
+ * python 3.13 compatibility
56
+
57
+
58
+ ## [0.7.0] 2024-08-01
59
+ ### Added
60
+ * poetry for project management
61
+ * utcnow() function utility for newer python versions
62
+
63
+ ## [0.6.0] 2023-08-11
64
+ ### Added
65
+ - Compatibility and tests for python versions 3.10 and 3.11
66
+
67
+ ### Removed
68
+ - Removed all cython stuff
69
+
70
+ ## [0.5.1] 2023-06-28
71
+ ### Fixed
72
+ - Prevent access to attribute if cache_value is None
73
+
74
+ ## [0.5.0] 2023-03-27
75
+ ### Fixed
76
+ - Pypy onecache works
77
+
78
+ ## [0.4.1] 2022-02-04
79
+ ### Fixed
80
+ - Counting values size at decreasing
81
+
82
+ ## [0.4.0] 2022-01-30
83
+ ### Added
84
+ - C extension for cache value with cython
85
+
86
+ ## [0.3.2] 2022-01-26
87
+ ### Added
88
+ - Flag for thread safety
89
+
90
+ ## [0.3.1] 2021-07-20
91
+ ### Changed
92
+ - Better pop when doing set, and reaching max size
93
+
94
+ ## [0.3.0] 2021-07-19
95
+ ### Changed
96
+ - access map removed in favor of using cache map + CacheValue access attribute
97
+
98
+ ## [0.2.0] 2021-07-19
99
+ ### Added
100
+ - CacheValue class as data container
101
+ - refresh_ttl argument, for refresh ttl per key access
102
+
103
+ ## [0.1.0] 2021-07-18
104
+ ### Fixed
105
+ - Better serialize keys with sort
106
+
107
+ ## [0.0.4] 2021-07-16
108
+ ### Fixed
109
+ - TTL and LRU usage combination works as expected
110
+
111
+ ## [0.0.3] 2021-07-16
112
+ ### Added
113
+ - LRUCache, using it by default for decorators
114
+
115
+ ## [0.0.2] 2021-07-16
116
+ ### Fix
117
+ - Readme for pypi
118
+
119
+ ## [0.0.1] 2021-07-16
120
+ ### Added
121
+ - Cache class
122
+ - Sync and Async decorators
123
+
124
+ [Unreleased]: https://github.com/sonic182/onecache/compare/0.9.0..HEAD
125
+ [0.9.0]: https://github.com/sonic182/onecache/compare/0.8.1..0.9.0
126
+ [0.8.1]: https://github.com/sonic182/onecache/compare/0.8.0..0.8.1
127
+ [0.8.0]: https://github.com/sonic182/onecache/compare/0.7.2..0.8.0
128
+ [0.7.2]: https://github.com/sonic182/onecache/compare/0.7.1..0.7.2
129
+ [0.7.1]: https://github.com/sonic182/onecache/compare/0.7.0..0.7.1
130
+ [0.7.0]: https://github.com/sonic182/onecache/compare/0.6.0..0.7.0
131
+ [0.6.0]: https://github.com/sonic182/onecache/compare/0.5.1..0.6.0
132
+ [0.5.1]: https://github.com/sonic182/onecache/compare/0.5.0..0.5.1
133
+ [0.5.0]: https://github.com/sonic182/onecache/compare/0.4.1..0.5.0
134
+ [0.4.1]: https://github.com/sonic182/onecache/compare/0.4.0..0.4.1
135
+ [0.4.0]: https://github.com/sonic182/onecache/compare/0.3.2..0.4.0
136
+ [0.3.2]: https://github.com/sonic182/onecache/compare/0.3.1..0.3.2
137
+ [0.3.1]: https://github.com/sonic182/onecache/compare/0.3.0..0.3.1
138
+ [0.3.0]: https://github.com/sonic182/onecache/compare/0.2.0..0.3.0
139
+ [0.2.0]: https://github.com/sonic182/onecache/compare/0.1.0..0.2.0
140
+ [0.1.0]: https://github.com/sonic182/onecache/compare/0.0.4..0.1.0
141
+ [0.0.4]: https://github.com/sonic182/onecache/compare/0.0.3..0.0.4
142
+ [0.0.3]: https://github.com/sonic182/onecache/compare/0.0.2..0.0.3
143
+ [0.0.2]: https://github.com/sonic182/onecache/compare/0.0.1..0.0.2
144
+ [0.0.1]: https://github.com/sonic182/onecache/compare/814065637987644cc56a09028df955e001a2163b..0.0.1
@@ -0,0 +1,7 @@
1
+ include README.md
2
+ include CHANGELOG.md
3
+ include pytest.ini
4
+ include onecache/*.pyx
5
+ recursive-include tests *.py
6
+ exclude onecache/*.c
7
+ global-exclude *.so *.pyd *.py[cod]
@@ -1,16 +1,14 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: onecache
3
- Version: 0.8.1
3
+ Version: 0.9.0
4
4
  Summary: Python cache for sync and async code
5
+ Author-email: Johanderson Mogollon <johander1822@gmail.com>
5
6
  License: MIT
6
- Author: Johanderson Mogollon
7
- Author-email: johander1822@gmail.com
8
- Requires-Python: >=3.8
7
+ Project-URL: Homepage, https://github.com/sonic182/onecache
9
8
  Classifier: Development Status :: 5 - Production/Stable
10
9
  Classifier: Intended Audience :: Developers
11
- Classifier: License :: OSI Approved :: MIT License
12
10
  Classifier: Operating System :: OS Independent
13
- Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Cython
14
12
  Classifier: Programming Language :: Python :: 3.8
15
13
  Classifier: Programming Language :: Python :: 3.9
16
14
  Classifier: Programming Language :: Python :: 3.10
@@ -18,8 +16,11 @@ Classifier: Programming Language :: Python :: 3.11
18
16
  Classifier: Programming Language :: Python :: 3.12
19
17
  Classifier: Programming Language :: Python :: 3.13
20
18
  Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Programming Language :: Python :: Implementation :: CPython
21
20
  Classifier: Programming Language :: Python :: Implementation :: PyPy
21
+ Requires-Python: >=3.8
22
22
  Description-Content-Type: text/markdown
23
+ Dynamic: license
23
24
 
24
25
 
25
26
  [![Coverage Status](https://coveralls.io/repos/github/sonic182/onecache/badge.svg?branch=master)](https://coveralls.io/github/sonic182/onecache?branch=master)
@@ -30,7 +31,9 @@ Python cache for sync and async code.
30
31
 
31
32
  Cache uses an LRU algorithm and can optionally set TTLs per entry.
32
33
 
33
- Tested automatically on CPython 3.8–3.14 and PyPy 3.9 across Linux, macOS, and Windows (see the workflow badge). Earlier versions may work but are not part of the supported matrix.
34
+ On CPython the cache core is a compiled Cython extension (prebuilt wheels for Linux, macOS and Windows on x86_64 and ARM64). On PyPy, or on platforms without a prebuilt wheel, it falls back to an equivalent pure Python implementation. `onecache.COMPILED` tells which one is in use.
35
+
36
+ Tested automatically on CPython 3.10 and 3.14 and PyPy 3.11 across Linux, macOS, and Windows on x86_64 and ARM64 (see the workflow badge). Python 3.8 and 3.9 are still installable and get prebuilt wheels, but are no longer part of the test matrix.
34
37
 
35
38
  # Usage
36
39
 
@@ -79,9 +82,15 @@ Decorator classes supports the following arguments
79
82
  * **cache_class (class)**: Class to use for cache instance. default: LRUCache
80
83
  * **refresh_ttl (bool)**: if cache with ttl, This flag makes key expiration timestamp to be refresh per access. default: False
81
84
  * **thread_safe (bool)**: tell decorator to use thread safe lock. default=False
82
- * **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.
85
+ * **max_mem_size (int)**: max mem size in bytes, as the sum of `sys.getsizeof(key) + sys.getsizeof(value)` of every entry (shallow sizes). default=None which means no limit. For pypy this value is ignored as the objects can change by the JIT compilation.
86
+
87
+ If num of records exceds maxsize, it drops the least recently used one. With `max_mem_size`, a value bigger than the limit is not cached.
83
88
 
84
- If num of records exceds maxsize, it drops the oldest.
89
+ Use `thread_safe=True` when the decorated function is called from several threads.
90
+
91
+ The decorated function exposes its cache as `.cache`: `.cache.expire(*args, **kwargs)` drops one entry, `.cache.clear()` drops all of them, and `.cache.cache` is a read-only view of the entries.
92
+
93
+ Set `ONECACHE_NO_EXTENSIONS` to any non-empty value to force the pure Python implementation. If the extension failed to build when installing from source (e.g. no compiler), pip may keep reusing that pure Python build from its cache; reinstall with `pip install --no-cache-dir --force-reinstall onecache` after installing a compiler.
85
94
 
86
95
 
87
96
  # Development
@@ -91,11 +100,23 @@ Install dependencies with Poetry (includes dev + test groups):
91
100
  poetry install --with test,dev
92
101
  ```
93
102
 
94
- Run the test suite and coverage locally:
103
+ Build the Cython extension in place (needed after any change to `onecache/_ccore.pyx`):
95
104
  ```bash
96
- poetry run pytest --cov
105
+ make build_ext
97
106
  ```
98
107
 
108
+ Run the test suite against the compiled and the pure Python implementations:
109
+ ```bash
110
+ poetry run pytest
111
+ ONECACHE_NO_EXTENSIONS=1 poetry run pytest
112
+ ```
113
+
114
+ `onecache/_ccore.pyx` and `onecache/_core.py` implement the same API and must be kept in sync.
115
+
116
+ # Releasing
117
+
118
+ Pull requests only build cp310 and cp314 wheels, so first run the `Wheels` workflow by hand on `master` (Actions → Wheels → Run workflow) to build and test every Python version. Then bump `version` in `pyproject.toml` and push a tag with the same value (e.g. `0.9.0`). The `Wheels` workflow builds the wheels with cibuildwheel, the sdist and a pure Python fallback wheel, and publishes them to PyPI through trusted publishing. That needs a `pypi` environment in the GitHub repository settings and a trusted publisher on PyPI for this repository, workflow `wheels.yml` and environment `pypi`.
119
+
99
120
  Lint and format checks:
100
121
  ```bash
101
122
  poetry run flake8
@@ -109,4 +130,3 @@ poetry run autopep8 --in-place --recursive onecache tests
109
130
  3. commit - push - pull request
110
131
 
111
132
  Thanks :)
112
-
@@ -0,0 +1,108 @@
1
+
2
+ [![Coverage Status](https://coveralls.io/repos/github/sonic182/onecache/badge.svg?branch=master)](https://coveralls.io/github/sonic182/onecache?branch=master)
3
+ ![github status](https://github.com/sonic182/onecache/actions/workflows/python.yml/badge.svg)
4
+ # OneCache
5
+
6
+ Python cache for sync and async code.
7
+
8
+ Cache uses an LRU algorithm and can optionally set TTLs per entry.
9
+
10
+ On CPython the cache core is a compiled Cython extension (prebuilt wheels for Linux, macOS and Windows on x86_64 and ARM64). On PyPy, or on platforms without a prebuilt wheel, it falls back to an equivalent pure Python implementation. `onecache.COMPILED` tells which one is in use.
11
+
12
+ Tested automatically on CPython 3.10 and 3.14 and PyPy 3.11 across Linux, macOS, and Windows on x86_64 and ARM64 (see the workflow badge). Python 3.8 and 3.9 are still installable and get prebuilt wheels, but are no longer part of the test matrix.
13
+
14
+ # Usage
15
+
16
+ ```python
17
+ from onecache import CacheDecorator
18
+ from onecache import AsyncCacheDecorator
19
+
20
+
21
+ class Counter:
22
+ def __init__(self, count=0):
23
+ self.count = count
24
+
25
+
26
+ @pytest.mark.asyncio
27
+ async def test_async_cache_counter():
28
+ """Test async cache, counter case."""
29
+ counter = Counter()
30
+
31
+ @AsyncCacheDecorator()
32
+ async def mycoro(counter: Counter):
33
+ counter.count += 1
34
+ return counter.count
35
+
36
+ assert 1 == (await mycoro(counter))
37
+ assert 1 == (await mycoro(counter))
38
+
39
+
40
+ def test_cache_counter():
41
+ """Test async cache, counter case."""
42
+ counter = Counter()
43
+
44
+ @CacheDecorator()
45
+ def sample(counter: Counter):
46
+ counter.count += 1
47
+ return counter.count
48
+
49
+ assert 1 == (sample(counter))
50
+ assert 1 == (sample(counter))
51
+ ```
52
+
53
+ Decorator classes supports the following arguments
54
+
55
+ * **maxsize (int)**: Maximun number of items to be cached. default: 512
56
+ * **ttl (int)**: time to expire in milliseconds, if None, it does not expire. default: None
57
+ * **skip_args (bool)**: apply cache as the function doesn't have any arguments, default: False
58
+ * **cache_class (class)**: Class to use for cache instance. default: LRUCache
59
+ * **refresh_ttl (bool)**: if cache with ttl, This flag makes key expiration timestamp to be refresh per access. default: False
60
+ * **thread_safe (bool)**: tell decorator to use thread safe lock. default=False
61
+ * **max_mem_size (int)**: max mem size in bytes, as the sum of `sys.getsizeof(key) + sys.getsizeof(value)` of every entry (shallow sizes). default=None which means no limit. For pypy this value is ignored as the objects can change by the JIT compilation.
62
+
63
+ If num of records exceds maxsize, it drops the least recently used one. With `max_mem_size`, a value bigger than the limit is not cached.
64
+
65
+ Use `thread_safe=True` when the decorated function is called from several threads.
66
+
67
+ The decorated function exposes its cache as `.cache`: `.cache.expire(*args, **kwargs)` drops one entry, `.cache.clear()` drops all of them, and `.cache.cache` is a read-only view of the entries.
68
+
69
+ Set `ONECACHE_NO_EXTENSIONS` to any non-empty value to force the pure Python implementation. If the extension failed to build when installing from source (e.g. no compiler), pip may keep reusing that pure Python build from its cache; reinstall with `pip install --no-cache-dir --force-reinstall onecache` after installing a compiler.
70
+
71
+
72
+ # Development
73
+
74
+ Install dependencies with Poetry (includes dev + test groups):
75
+ ```bash
76
+ poetry install --with test,dev
77
+ ```
78
+
79
+ Build the Cython extension in place (needed after any change to `onecache/_ccore.pyx`):
80
+ ```bash
81
+ make build_ext
82
+ ```
83
+
84
+ Run the test suite against the compiled and the pure Python implementations:
85
+ ```bash
86
+ poetry run pytest
87
+ ONECACHE_NO_EXTENSIONS=1 poetry run pytest
88
+ ```
89
+
90
+ `onecache/_ccore.pyx` and `onecache/_core.py` implement the same API and must be kept in sync.
91
+
92
+ # Releasing
93
+
94
+ Pull requests only build cp310 and cp314 wheels, so first run the `Wheels` workflow by hand on `master` (Actions → Wheels → Run workflow) to build and test every Python version. Then bump `version` in `pyproject.toml` and push a tag with the same value (e.g. `0.9.0`). The `Wheels` workflow builds the wheels with cibuildwheel, the sdist and a pure Python fallback wheel, and publishes them to PyPI through trusted publishing. That needs a `pypi` environment in the GitHub repository settings and a trusted publisher on PyPI for this repository, workflow `wheels.yml` and environment `pypi`.
95
+
96
+ Lint and format checks:
97
+ ```bash
98
+ poetry run flake8
99
+ poetry run autopep8 --in-place --recursive onecache tests
100
+ ```
101
+
102
+ # Contribute
103
+
104
+ 1. Fork
105
+ 2. create a branch `feature/your_feature`
106
+ 3. commit - push - pull request
107
+
108
+ Thanks :)
@@ -0,0 +1,122 @@
1
+ """Onecache main module."""
2
+
3
+ import os
4
+ from functools import update_wrapper
5
+ from typing import Optional
6
+
7
+ if os.environ.get("ONECACHE_NO_EXTENSIONS"):
8
+ from onecache._core import CacheValue, ExpirableCache, LRUCache, monotonic
9
+
10
+ COMPILED = False
11
+ else:
12
+ try:
13
+ from onecache._ccore import CacheValue, ExpirableCache, LRUCache, monotonic
14
+
15
+ COMPILED = True
16
+ except ImportError:
17
+ from onecache._core import CacheValue, ExpirableCache, LRUCache, monotonic
18
+
19
+ COMPILED = False
20
+
21
+ __all__ = [
22
+ "AsyncCacheDecorator",
23
+ "CacheDecorator",
24
+ "CacheValue",
25
+ "COMPILED",
26
+ "ExpirableCache",
27
+ "LRUCache",
28
+ "monotonic",
29
+ ]
30
+
31
+ _MISSING = object()
32
+
33
+
34
+ class CacheDecorator:
35
+ """Decorator for ExpirableCache"""
36
+
37
+ def __init__(
38
+ self,
39
+ maxsize=512,
40
+ ttl: Optional[int] = None,
41
+ skip_args: bool = False,
42
+ cache_class=LRUCache,
43
+ refresh_ttl: Optional[bool] = False,
44
+ thread_safe: Optional[bool] = False,
45
+ max_mem_size: Optional[int] = None,
46
+ ):
47
+ """
48
+ Args:
49
+ * **maxsize (int)**: Maximun number of items to be cached. default: 512
50
+ * **ttl (int)**: time to expire in milliseconds, if None, it does not expire. default: None
51
+ * **skip_args (bool)**: apply cache as the function doesn't have any arguments, default: False
52
+ * **cache_class (class)**: Class to use for cache instance. default: LRUCache
53
+ * **refresh_ttl (bool)**: if cache with ttl, This flag makes key expiration timestamp to be
54
+ refresh per access. default: False
55
+ * **thread_safe (bool)**: tell decorator to use thread safe lock. default=False
56
+ * **max_mem_size (int)**: max mem size inside cache structure. default=None which means no limit.
57
+ For pypy this value is ignored as the objects can change by the JIT compilation.
58
+ """
59
+ self.cache = cache_class(
60
+ maxsize,
61
+ ttl,
62
+ refresh_ttl=refresh_ttl,
63
+ thread_safe=thread_safe,
64
+ max_mem_size=max_mem_size,
65
+ )
66
+ self.maxsize = maxsize
67
+ self.skip_args = skip_args
68
+ self._fast = type(self.cache) in (ExpirableCache, LRUCache)
69
+
70
+ def __call__(self, func):
71
+ cache = self.cache
72
+ skip_args = self.skip_args
73
+
74
+ if self._fast:
75
+ return update_wrapper(cache._wrap(func, skip_args), func, updated=())
76
+
77
+ def wrapper(*args, **kwargs):
78
+ key = "any" if skip_args else cache.get_key(*args, **kwargs)
79
+ if key not in cache:
80
+ resp = func(*args, **kwargs)
81
+ cache.set(key, resp)
82
+ return resp
83
+ return cache.get(key)
84
+
85
+ wrapper.cache = cache
86
+
87
+ return update_wrapper(wrapper, func, updated=())
88
+
89
+
90
+ class AsyncCacheDecorator(CacheDecorator):
91
+ """Async Decorator for ExpirableCache"""
92
+
93
+ def __call__(self, func):
94
+ cache = self.cache
95
+ skip_args = self.skip_args
96
+
97
+ if self._fast:
98
+ make_key = cache._make_key
99
+ lookup = cache._lookup
100
+ set_value = cache.set
101
+
102
+ async def async_wrapper(*args, **kwargs):
103
+ key = "any" if skip_args else make_key(args, kwargs)
104
+ value = lookup(key, _MISSING)
105
+ if value is _MISSING:
106
+ value = await func(*args, **kwargs)
107
+ set_value(key, value)
108
+ return value
109
+
110
+ else:
111
+
112
+ async def async_wrapper(*args, **kwargs):
113
+ key = "any" if skip_args else cache.get_key(*args, **kwargs)
114
+ if key not in cache:
115
+ resp = await func(*args, **kwargs)
116
+ cache.set(key, resp)
117
+ return resp
118
+ return cache.get(key)
119
+
120
+ async_wrapper.cache = cache
121
+
122
+ return update_wrapper(async_wrapper, func, updated=())