small-cache 0.1.3__cp315-cp315-win_amd64.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.
- small_cache/__init__.py +14 -0
- small_cache/_small_cache_impl.cp315-win_amd64.pyd +0 -0
- small_cache/_small_cache_impl.pyi +36 -0
- small_cache/py.typed +0 -0
- small_cache-0.1.3.dist-info/DELVEWHEEL +2 -0
- small_cache-0.1.3.dist-info/METADATA +192 -0
- small_cache-0.1.3.dist-info/RECORD +10 -0
- small_cache-0.1.3.dist-info/WHEEL +5 -0
- small_cache-0.1.3.dist-info/licenses/LICENSE +26 -0
- small_cache.libs/msvcp140-a4c2229bdc2a2a630acdc095b4d86008.dll +0 -0
small_cache/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""""" # start delvewheel patch
|
|
2
|
+
def _delvewheel_patch_1_13_1():
|
|
3
|
+
import os
|
|
4
|
+
if os.path.isdir(libs_dir := os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, 'small_cache.libs'))):
|
|
5
|
+
os.add_dll_directory(libs_dir)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
_delvewheel_patch_1_13_1()
|
|
9
|
+
del _delvewheel_patch_1_13_1
|
|
10
|
+
# end delvewheel patch
|
|
11
|
+
|
|
12
|
+
from ._small_cache_impl import SmallCache
|
|
13
|
+
|
|
14
|
+
__all__ = ["SmallCache"]
|
|
Binary file
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class SmallCache:
|
|
5
|
+
def __init__(self, attribute_names: Sequence[str]) -> None:
|
|
6
|
+
"""A cache of items with up to 96 attributes, named in this order."""
|
|
7
|
+
|
|
8
|
+
def begin_transaction(self, estimated_number_of_items: int = 0, remove_old_items: bool = True) -> None:
|
|
9
|
+
"""
|
|
10
|
+
Start a load. Readers keep seeing the current data until end_transaction(). With remove_old_items, items not added during the load are dropped.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def end_transaction(self) -> None:
|
|
14
|
+
"""Publish the load to readers."""
|
|
15
|
+
|
|
16
|
+
def abort_transaction(self) -> None:
|
|
17
|
+
"""Discard the load; readers keep the current data."""
|
|
18
|
+
|
|
19
|
+
def add(self, item_id: str, attributes: dict[str, None | bool | int | float | str | bytes | list[str | bytes] | tuple[str | bytes, ...]]) -> None:
|
|
20
|
+
"""
|
|
21
|
+
Add or replace an item in the open load. bytes are JSON text, embedded in answers as is; attributes not in the cache's names are ignored.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def get_one(self, id: str, attributes: Sequence[str]) -> bytes:
|
|
25
|
+
"""
|
|
26
|
+
JSON array of the item's values in the order of `attributes` (null if unset); [] if there is no such item.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def get_many(self, ids: Sequence[str], attributes: Sequence[str]) -> bytes:
|
|
30
|
+
"""JSON array with a get_one() row for each id."""
|
|
31
|
+
|
|
32
|
+
def get_all(self, attributes: Sequence[str]) -> bytes:
|
|
33
|
+
"""JSON array with a get_one() row for each item, in get_all_ids() order."""
|
|
34
|
+
|
|
35
|
+
def get_all_ids(self) -> bytes:
|
|
36
|
+
"""JSON array of all item ids."""
|
small_cache/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
Version: 1.13.1
|
|
2
|
+
Arguments: ['C:\\Users\\runneradmin\\AppData\\Local\\Temp\\cibw-run-tonu1i82\\cp315-win_amd64\\build\\venv\\Scripts\\delvewheel', 'repair', '-w', 'C:\\Users\\runneradmin\\AppData\\Local\\Temp\\cibw-run-tonu1i82\\cp315-win_amd64\\repaired_wheel', '-v', 'C:\\Users\\runneradmin\\AppData\\Local\\Temp\\cibw-run-tonu1i82\\cp315-win_amd64\\built_wheel\\small_cache-0.1.3-cp315-cp315-win_amd64.whl']
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: small-cache
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: Compact in-process snapshot cache for structured items
|
|
5
|
+
Author: kam1k4dze
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Project-URL: Homepage, https://github.com/kam1k4dze/small_cache
|
|
9
|
+
Requires-Python: <3.16,>=3.15
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# small_cache
|
|
13
|
+
|
|
14
|
+
A memory-efficient in-process cache for structured items with typed attributes, implemented in C++23 and exposed to Python via [nanobind](https://github.com/wjakob/nanobind).
|
|
15
|
+
|
|
16
|
+
| CI | Status |
|
|
17
|
+
|----|--------|
|
|
18
|
+
| wheels | [![Wheel Action Status][actions-wheels-badge]][actions-wheels-link] |
|
|
19
|
+
|
|
20
|
+
[actions-wheels-link]: https://github.com/kam1k4dze/small_cache/actions?query=workflow%3AWheels
|
|
21
|
+
[actions-wheels-badge]: https://github.com/kam1k4dze/small_cache/workflows/Wheels/badge.svg
|
|
22
|
+
|
|
23
|
+
## Features
|
|
24
|
+
|
|
25
|
+
- Store items with up to **96 named attributes** per cache instance
|
|
26
|
+
- Supported attribute types: `bool`, `int` (64-bit), `float`, `str`, JSON (`bytes`), lists of `str`/JSON, `None` —
|
|
27
|
+
each comes back as the type it was stored as
|
|
28
|
+
- **JSON answers** — getters return JSON written directly from the cache, with stored JSON embedded as is
|
|
29
|
+
- **Flat, compact storage** — every distinct string is stored once; strings of 64+ bytes are zstd-compressed
|
|
30
|
+
with a dictionary trained on the data; each item is one packed record (absent and `None` attributes take no space)
|
|
31
|
+
- **Snapshots** — a reload is built aside and published at once; reads never block and never see a half-finished load
|
|
32
|
+
- **Transaction model** — bulk-update the cache, optionally evicting items the reload did not contain
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install small-cache
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Only binary wheels are published, for CPython 3.15, regular and free-threaded (3.15t), on Linux, macOS and
|
|
41
|
+
Windows (64-bit). With free-threading the cache runs without the GIL: reads and loading proceed in parallel.
|
|
42
|
+
|
|
43
|
+
Build from source:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
git clone https://github.com/kam1k4dze/small_cache
|
|
47
|
+
pip install ./small_cache
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Requirements:** Python ≥ 3.15 and < 3.16; to build: CMake ≥ 3.28 and a C++23 compiler
|
|
51
|
+
(build dependencies: scikit-build-core 1.1.1, nanobind 3.1.0)
|
|
52
|
+
|
|
53
|
+
## Quick Start
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from small_cache import SmallCache
|
|
57
|
+
|
|
58
|
+
# Declare the attributes you care about (max 96)
|
|
59
|
+
cache = SmallCache(["name", "price", "in_stock", "tags"])
|
|
60
|
+
|
|
61
|
+
# A transaction wraps a full cache refresh cycle
|
|
62
|
+
cache.begin_transaction(estimated_number_of_items=1000)
|
|
63
|
+
|
|
64
|
+
cache.add("sku-001", {
|
|
65
|
+
"name": "Widget",
|
|
66
|
+
"price": 9.99,
|
|
67
|
+
"in_stock": True,
|
|
68
|
+
"tags": ["sale", "hardware"],
|
|
69
|
+
})
|
|
70
|
+
cache.add("sku-002", {"name": "Gadget", "price": 24.99, "in_stock": False})
|
|
71
|
+
|
|
72
|
+
cache.end_transaction() # publishes the new data; items not re-added are evicted
|
|
73
|
+
|
|
74
|
+
# Answers are JSON bytes
|
|
75
|
+
cache.get_one("sku-001", ["name", "price"])
|
|
76
|
+
# b'["Widget",9.99]'
|
|
77
|
+
|
|
78
|
+
# Batch retrieval: an unknown id gets an empty row
|
|
79
|
+
cache.get_many(["sku-001", "sku-002", "sku-999"], ["name", "in_stock"])
|
|
80
|
+
# b'[["Widget",true],["Gadget",false],[]]'
|
|
81
|
+
|
|
82
|
+
cache.get_all_ids()
|
|
83
|
+
# b'["sku-001","sku-002"]'
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Transaction Model
|
|
87
|
+
|
|
88
|
+
`begin_transaction()` / `end_transaction()` bracket a full reload cycle:
|
|
89
|
+
|
|
90
|
+
- Items written with `add()` go into a new snapshot that readers cannot see yet. Until
|
|
91
|
+
`end_transaction()`, every read is served from the current snapshot.
|
|
92
|
+
- `end_transaction()` publishes the new snapshot at once. Items that were **not** written in the transaction
|
|
93
|
+
are dropped, or carried over unchanged with `remove_old_items=False`.
|
|
94
|
+
- `abort_transaction()` throws the new snapshot away; the current one keeps being served. Call it when a
|
|
95
|
+
reload fails half-way, so a partial load is never published.
|
|
96
|
+
- Reads take no lock and can run from any thread. While a reload is being built, the old and the new
|
|
97
|
+
snapshot are both in memory.
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
# Keep stale items (incremental update)
|
|
101
|
+
cache.begin_transaction(remove_old_items=False)
|
|
102
|
+
cache.add("sku-003", {"name": "New item", "price": 5.0})
|
|
103
|
+
cache.end_transaction() # pre-existing items survive
|
|
104
|
+
|
|
105
|
+
# Never publish a failed reload
|
|
106
|
+
cache.begin_transaction()
|
|
107
|
+
try:
|
|
108
|
+
for item_id, attributes in fetch_items():
|
|
109
|
+
cache.add(item_id, attributes)
|
|
110
|
+
except Exception:
|
|
111
|
+
cache.abort_transaction()
|
|
112
|
+
raise
|
|
113
|
+
cache.end_transaction()
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## API Reference
|
|
117
|
+
|
|
118
|
+
### `SmallCache(attribute_names: list[str])`
|
|
119
|
+
|
|
120
|
+
Construct a cache instance. `attribute_names` lists every attribute that may be stored. Maximum 96 attributes; raises `RuntimeError` if the list is empty or exceeds the limit.
|
|
121
|
+
|
|
122
|
+
### `begin_transaction(estimated_number_of_items: int = 0, remove_old_items: bool = True)`
|
|
123
|
+
|
|
124
|
+
Open a write transaction. Optionally pre-allocates capacity. Raises `RuntimeError` if a transaction is already open.
|
|
125
|
+
|
|
126
|
+
### `end_transaction()`
|
|
127
|
+
|
|
128
|
+
Publish the transaction. With `remove_old_items=True` (default), items not written during this transaction are evicted. Raises `RuntimeError` if no transaction is open.
|
|
129
|
+
|
|
130
|
+
### `abort_transaction()`
|
|
131
|
+
|
|
132
|
+
Discard the transaction; the cache keeps serving what it had. Raises `RuntimeError` if no transaction is open.
|
|
133
|
+
|
|
134
|
+
### `add(item_id: str, attributes: dict[str, None | bool | int | float | str | bytes | list[str | bytes]])`
|
|
135
|
+
|
|
136
|
+
Insert or replace an item. Must be called inside a transaction. Attributes absent from `attribute_names` are
|
|
137
|
+
silently ignored. `bytes` is JSON text, kept as is: the getters write it into their answers unchanged, so it
|
|
138
|
+
must be valid JSON. List items are `str` or such JSON `bytes`. An `int` is stored as an integer and a `float` as
|
|
139
|
+
a float; any other type, or an `int` outside the signed 64-bit range, raises `TypeError`.
|
|
140
|
+
|
|
141
|
+
### `get_one(id: str, attributes: list[str]) -> bytes`
|
|
142
|
+
|
|
143
|
+
The item's values for `attributes`, in that order, as a UTF-8 JSON array. Attributes that were not set, or whose
|
|
144
|
+
name is not registered, are `null`; stored JSON is embedded as is; floats always carry a decimal point or exponent,
|
|
145
|
+
so they stay floats. `[]` if the item does not exist or `attributes` is empty. The answer is built without holding
|
|
146
|
+
the GIL. To return it from a JSON API without decoding it, wrap it, e.g. in `msgspec.Raw`.
|
|
147
|
+
|
|
148
|
+
### `get_many(ids: list[str], attributes: list[str]) -> bytes`
|
|
149
|
+
|
|
150
|
+
A JSON array with one `get_one` row per requested id.
|
|
151
|
+
|
|
152
|
+
### `get_all(attributes: list[str]) -> bytes`
|
|
153
|
+
|
|
154
|
+
A JSON array with one `get_one` row per item, in `get_all_ids()` order.
|
|
155
|
+
|
|
156
|
+
### `get_all_ids() -> bytes`
|
|
157
|
+
|
|
158
|
+
A JSON array of all item ids: the order items were added in, items carried over by `remove_old_items=False` last.
|
|
159
|
+
|
|
160
|
+
## Building from Source
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
git clone https://github.com/kam1k4dze/small_cache
|
|
164
|
+
cd small_cache
|
|
165
|
+
pip install -e .
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Native C++ unit tests and benchmark (requires CMake ≥ 3.28, Ninja and a C++23 compiler with modules support):
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
cmake -B build -G Ninja && cmake --build build
|
|
172
|
+
./build/small_cache_native_test
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
`small_cache_bench` replays a recording of `add()` calls (JSON lines: a header `{"attributes": [...]}`, then one
|
|
176
|
+
`{"id": ..., "a": {...}}` per call) as a series of reloads and reports memory and timings:
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
./build/small_cache_bench adds.jsonl --cycles 3
|
|
180
|
+
heaptrack ./build/small_cache_bench adds.jsonl --cycles 2
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Python tests:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
pip install pytest
|
|
187
|
+
pytest tests/
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## License
|
|
191
|
+
|
|
192
|
+
BSD-3-Clause — see [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
small_cache/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
small_cache/_small_cache_impl.cp315-win_amd64.pyd,sha256=D6h2LZW9Q-4VQCC0efPVomwQLs_C175cTLvGApRdFVQ,696320
|
|
3
|
+
small_cache/_small_cache_impl.pyi,sha256=PZKFo2sK-JDfIo8vAHfnlAF6vPRDDm18oVIjkBNWp_o,1602
|
|
4
|
+
small_cache/__init__.py,sha256=HJIgqC465XmEv0_efSyD5_zN0Ya6vdKbTla4o7i1ftI,404
|
|
5
|
+
small_cache-0.1.3.dist-info/DELVEWHEEL,sha256=7Bfnufz_8-yCmxIqCBvK11Hx7ejOBKQ6mnKzE5llErU,410
|
|
6
|
+
small_cache-0.1.3.dist-info/METADATA,sha256=C23pQJaktgo4OpIFM-skXTarGdIZrVFBnIgaX_lFF64,7171
|
|
7
|
+
small_cache-0.1.3.dist-info/RECORD,,
|
|
8
|
+
small_cache-0.1.3.dist-info/WHEEL,sha256=lX_s9_PODHQdPEziwqn30Su2CEVeLs-HsvaJqybfX_g,105
|
|
9
|
+
small_cache-0.1.3.dist-info/licenses/LICENSE,sha256=zBrz9KYdq8ONytsas0TAkOopMFXkrfGHqbyieoAEHo0,1545
|
|
10
|
+
small_cache.libs/msvcp140-a4c2229bdc2a2a630acdc095b4d86008.dll,sha256=pMIim9wqKmMKzcCVtNhgCOXD47x3cxdDVPPaT1vrnN4,575056
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Copyright (c) 2025-2026 kam1k4dze <me@kam1k4dze.com>, All rights reserved.
|
|
2
|
+
|
|
3
|
+
Redistribution and use in source and binary forms, with or without
|
|
4
|
+
modification, are permitted provided that the following conditions are met:
|
|
5
|
+
|
|
6
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
7
|
+
list of conditions and the following disclaimer.
|
|
8
|
+
|
|
9
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
10
|
+
this list of conditions and the following disclaimer in the documentation
|
|
11
|
+
and/or other materials provided with the distribution.
|
|
12
|
+
|
|
13
|
+
3. Neither the name of the copyright holder nor the names of its contributors
|
|
14
|
+
may be used to endorse or promote products derived from this software
|
|
15
|
+
without specific prior written permission.
|
|
16
|
+
|
|
17
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
18
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
19
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
20
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
21
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
22
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
23
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
24
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
25
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
26
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
Binary file
|