Durable file-backed caching for JSON-like data with pluggable storage backends
Project description
PyperCache
A Python library providing durable file-backed caching for JSON-like data with pluggable storage backends (pickle, JSON, chunked manifest, SQLite), optional TTL and staleness semantics, read-only query navigation, and append-only request logging.
Installation
pip install pypercache
Or install from source:
git clone https://github.com/BrandonBahret/PyperCache.git
cd PyperCache
pip install .
Quick Start
See the full documentation, examples, and API reference on GitHub:
https://github.com/BrandonBahret/PyperCache/tree/master/docs
Features
- Pluggable Backends: Choose storage by file extension (.pkl, .json, .manifest, .db)
- TTL & Staleness: Optional expiry and acceptable staleness windows
- Typed Objects: Decorate classes for automatic serialization/deserialization
- Query Navigation: Safe, read-only JSON path queries with filters
- Request Logging: Thread-safe JSONL audit trails
Testing
pytest
Example
The snippet below demonstrates every major feature in one pass: choosing a backend, TTL, typed objects, query navigation, and request logging.
import math
from pypercache import Cache, RequestLogger
from pypercache.models.apimodel import apimodel
# ── 1. Backend is chosen by file extension ──────────────────────────────────
cache = Cache(filepath="api-cache.db") # .pkl / .json / .manifest / .db
log = RequestLogger("api_requests.log")
# ── 2. Define a typed model ──────────────────────────────────────────────────
@apimodel
class SearchResult:
total: int
hits: list
# ── 3. Fetch-or-cache pattern ────────────────────────────────────────────────
KEY = "search:v1:python"
if not cache.is_data_fresh(KEY):
payload = {
"total": 3,
"hits": [
{"name": "Alice", "role": "staff", "score": 92},
{"name": "Bob", "role": "guest", "score": 74},
{"name": "Carol", "role": "staff", "score": 88},
],
}
cache.store(KEY, payload, expiry=3600, cast=SearchResult)
log.log(uri="/api/search?q=python", status=200)
# ── 4. Retrieve a typed object ───────────────────────────────────────────────
result: SearchResult = cache.get_object(KEY) # SearchResult instance
print(result.total) # 3
# ── 5. Query without mutating the payload ───────────────────────────────────
q = cache.get(KEY).query
print(q.get("total")) # 3
print(q.get("hits?role=staff.name")) # [Alice, Carol]
print(q.get("hits?name*")) # ['Alice', 'Bob', 'Carol']
print(q.get("hits?role=staff", select_first=True)["name"]) # 'Alice'
# ── 6. Inspect the request log ───────────────────────────────────────────────
for entry in log.get_logs_from_last_seconds(60):
print(entry.data["uri"], entry.data["status"])
Features
- Four backends —
.pkl,.json,.manifest,.db(SQLite with write-behind batching) - TTL & staleness — per-record expiry;
is_data_freshtells you whether to re-fetch - Typed round-trips —
@Cache.cached/@apimodel+cast=on store;get_object()on retrieval - Query navigation — dotted paths,
?key=valuefilters,?key*plucks,?keyexistence,select_first, defaults; all in memory over the loaded record - Request logging — thread-safe JSONL audit trail with time-window reads
Query navigation
record.query returns a JsonInjester — a lightweight, read-only selector language that runs in memory over the loaded payload. It never touches the storage backend.
q = cache.get("search:v1:python").query
You can also instantiate it directly over any dict:
from pypercache.query import JsonInjester
q = JsonInjester({"meta": {"total": 5}, "hits": [...]})
Path navigation
Dot-separated keys walk the dict. Returns UNSET if any key along the path is absent.
q.get("meta.total") # 5
q.get("meta.page") # 1
q.get("meta.missing") # UNSET
q.has("meta.total") # True (shorthand for `get(...) is not UNSET`)
Keys containing hyphens or other non-identifier characters must be wrapped in double quotes inside the selector string:
q.get('"content-type".value')
?key=value — match filter
Returns every element in a list where the key equals the value. A tail path after the operator plucks a field from each matched element.
q.get("hits?role=staff")
# [{"name": "Alice", ...}, {"name": "Carol", ...}]
q.get("hits?role=staff.name")
# ["Alice", "Carol"]
q.get("hits?team.name=Engineering")
# all dicts where hits[i].team.name == "Engineering"
Prefix the value with # to match numbers instead of strings:
q.get("hits?score=#92") # integer match
q.get("hits?ratio=#0.75") # float match
No matches returns an empty list, not UNSET.
?key* — pluck
Extracts a field from every element in the list. Non-missing results are collected; missing ones are silently skipped. Plucks can be chained.
q.get("hits?name*")
# ["Alice", "Bob", "Carol"]
q.get("hits?team.name*")
# ["Engineering", "Marketing", "Engineering"]
q.get("hits?role*?label*")
# chained: pluck role objects, then pluck label from each
On a dict cursor (rather than a list), pluck navigates to the key and returns its value or UNSET.
?key — exists filter
Does not extract values. On a list cursor, returns only elements that contain the key. On a dict cursor, returns the cursor unchanged if the key is present, or UNSET if absent.
# list cursor — filter to elements that have a "team" key
q.get("hits?team")
# dict cursor — gate on key presence
q.get("meta?total") # returns the meta dict (key exists)
q.get("meta?ghost") # UNSET
q.get("meta?ghost", default_value=0) # 0
select_first and default_value
select_first=True unwraps the first element of a list result. Returns UNSET if the list is empty.
from pypercache.query.json_injester import UNSET
first = q.get("hits?role=staff", select_first=True)
print(first["name"]) # "Alice"
empty = q.get("hits?role=contractor", select_first=True)
print(empty is UNSET) # True
default_value is returned when the path is missing or resolves to None. Falsy non-None values (False, 0, "") pass through unchanged.
q.get("meta.missing", default_value=0) # 0
q.get("flags.debug", default_value=False) # False (returned as-is, not default)
cast
When the result is a dict, cast passes it to the given type before returning.
q.get("hits?role=staff", select_first=True, cast=StaffMember)
# StaffMember instance
Known limitations
JsonInjester is intentionally scoped and simple. A few things it does not do:
- Integer list indexing —
"hits.0.name"is not supported. Use a filter or pluck to reach list elements. - Cross-key queries —
record.queryoperates on a single loaded payload. It does not scan multiple records or touch the backend. - Non-ASCII keys — unquoted non-ASCII key names raise a parse error. Wrap them in double quotes:
'"héros".name'.
For the complete selector reference see QUERY.md.
Documentation
| Topic | File |
|---|---|
Cache, CacheRecord, TTL, typed objects |
CACHE.md |
JsonInjester / record.query selector syntax |
QUERY.md |
Storage backends, RequestLogger, SQLite internals |
STORAGE.md |
Full docs and examples: https://github.com/BrandonBahret/PyperCache/tree/master/docs
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pypercache-0.1.4.tar.gz.
File metadata
- Download URL: pypercache-0.1.4.tar.gz
- Upload date:
- Size: 59.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ba72b1dd2e6de3e36ad094ac5e243b300346f43e39a0eedb22d1d57cb531db88
|
|
| MD5 |
d9b629a0ccba56fcc4d4b8598c6a7a86
|
|
| BLAKE2b-256 |
687925256c1fe21f400c4df0d7bda9909130081c1fda82b56b2034aea5167b2d
|
File details
Details for the file pypercache-0.1.4-py3-none-any.whl.
File metadata
- Download URL: pypercache-0.1.4-py3-none-any.whl
- Upload date:
- Size: 38.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b23b0179d13b58c9c2fa5bd0a7080176d4c7eb32247b1051c4f86d66c2b61a63
|
|
| MD5 |
d0f3a4f3aa6e6cdf6abe947c63af136a
|
|
| BLAKE2b-256 |
8f3a857895f395f2f6225cb5166c0b59a5547ab7a05da8744e6235c5d91c5d88
|