readerkit
Cached HTTP sessions, cache-directory resolution, and a bulk artifact cache for data readers.
Every cache here has an off switch, and a refresh reaches every readerkit cache in the stack whatever the intervening APIs offer.
Installation
readerkit reaches PyPI with its first release. Until then, install it from the repository.
uv add git+https://github.com/ONEcampaign/readerkit
readerkit requires Python 3.12 or later.
Usage
Cache-directory resolution
resolve_cache_dir turns a kwarg, an environment variable, or platformdirs into an absolute,
per-app, per-version cache root. Every other surface below takes its cache_dir from here.
from readerkit import resolve_cache_dir
cache_dir = resolve_cache_dir(app="imf-reader", app_version="1.5.2")
# .../v1/imf-reader/1.5.2, under $IMF_READER_CACHE_DIR, $BBLOCKS_CACHE_DIR, or
# platformdirs.user_cache_dir("readerkit"), in that order.
resolve_cache_dir only resolves a path. To switch caching off, pass cache_dir=None to the
surfaces that consume it.
HTTP sessions
build_session returns a configured requests session with bounded retries and jittered
backoff, an enforced default timeout, one redirect policy, optional per-host rate limiting, and
HTTP response caching when you give it a cache directory.
from readerkit import build_session
session = build_session(app="imf-reader", cache_dir=cache_dir)
response = session.get("https://sdmxcentral.imf.org/...")
Pass cache_dir=None to switch the response cache off. Retries, timeout, pooling and rate
limiting stay identical.
Build a session inside each multiprocessing worker process. A session built before a fork
shares its socket descriptors with the child, so a response can be delivered to the wrong
process. Every session carries a fork guard that raises SessionForkError the first time it is
used from a process other than the one that built it.
Bulk artifact cache
ArtifactCache caches large binary payloads (zips, parquet files, whatever a fetcher writes)
under per-entry sidecar metadata, with per-key locking and TTL-based staleness.
from datetime import timedelta
from readerkit import ArtifactCache, bulk_fetcher
cache = ArtifactCache(cache_dir=cache_dir, namespace="weo_sdmx")
path = cache.ensure(
"weo_2026_04",
fetcher=bulk_fetcher("https://.../weo_2026_04.zip", session=session),
ttl=timedelta(days=60),
version="4",
)
Pass cache_dir=None to switch the cache off. Every call then downloads fresh into a scratch
directory owned by the cache instance, and managed cache directories are untouched. Call
close(), or use the cache as a context manager, to clean the scratch directory up.
with ArtifactCache(cache_dir=None, namespace="weo_sdmx") as cache:
path = cache.ensure("weo_2026_04", fetcher=bulk_fetcher(url, session=session))
Cache keys
cache_key and cache_key_for_call derive a deterministic cache key from a function's fully
bound arguments, so a defaulted parameter stays in the key. oda-reader once hand-listed its key
components and left pre_process out, and two different preprocessing options returned
identical, wrong data.
from readerkit import cache_key_for_call
def read_gdp(*, country: str, start_year: int, pre_process: bool = True):
key = cache_key_for_call(
read_gdp, country=country, start_year=start_year, pre_process=pre_process
)
...
Two calls that differ only in pre_process produce different keys, whether the caller passed it
or relied on the default. Use the lower-level cache_key(parts=...) where you assemble the key
parts yourself, for example from a URL and a schema version.
Forcing a refresh through layered caches
refresh_scope() forces one refresh of every readerkit-cached artifact and response touched
inside the block. It reaches any cache built on readerkit, however deep.
import readerkit
with readerkit.refresh_scope():
df = (
pydeflate.read_weo()
) # refreshes pydeflate's own artifact, and imf-reader's underneath
refresh_scope() takes effect only for the duration of the with block.
A cached session also accepts refresh= and force_refresh= directly, for a bare call outside
any refresh_scope(). refresh=True sends a conditional request, so a 304 returns the cached
body unchanged. force_refresh=True issues a new request every time and overwrites whatever was
cached. refresh_scope() upgrades to force_refresh, so a caller asking for fresh data gets a
new response even from a server whose validators are unreliable.
session.get(url, refresh=True) # revalidate, may reuse the cached body
session.get(url, force_refresh=True) # always hits the server, overwrites the cache
Error handling
Every exception readerkit raises subclasses ReaderkitError. Catch that one name to handle
anything from the library at once, or catch a specific subclass to handle one failure mode:
ConfigurationErrorfor invalid arguments, raised eagerly at call time before any I/O.CacheDirectoryErrorwhen a cache directory cannot be resolved, created, or written to.ArtifactCacheErrorand its subclasses (ArtifactWriteError,ArtifactCorruptError,CacheLockTimeout,CacheLockUnavailable) for failures fromArtifactCache.TransportErrorand its subclasses (SessionForkError,RedirectPolicyError,TruncatedDownloadError) for failures from abuild_sessionsession.
Each carries an is_retryable class attribute, so a caller can branch on exc.is_retryable:
from readerkit import ReaderkitError
try:
path = cache.ensure(key, fetcher=fetcher)
except ReaderkitError as exc:
if exc.is_retryable:
path = cache.ensure(key, fetcher=fetcher, refresh=True)
else:
raise
HTTP status errors pass through unwrapped. Call raise_for_status() yourself and handle
requests.HTTPError, which keeps the status code intact.
Development
readerkit uses uv for dependency management. From a checkout:
uv sync --group dev
Running tests
uv run pytest
Code quality
uv run ruff check .
uv run ruff format .
uv run ty check src/readerkit
Building
uv build
Pre-commit hooks
Pre-commit hooks run on every commit once pre-commit install has been run. To run them across
the whole tree:
pre-commit run --all-files
License
readerkit is licensed under the MIT License. See the LICENSE file for details.
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 readerkit-0.1.0.tar.gz.
File metadata
- Download URL: readerkit-0.1.0.tar.gz
- Upload date:
- Size: 38.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.3 {"installer":{"name":"uv","version":"0.12.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fc0611ce5e0436d95d5600bb07fb413fc13452392fc04d1fc3692dde6b21a37f
|
|
| MD5 |
00ece2a8ac18b5ff72be5c39bc4a0214
|
|
| BLAKE2b-256 |
2d55dc54f3cc9556bbb733c2546c0c1f00ba4f0577131877560a3ba47e5ceeab
|
File details
Details for the file readerkit-0.1.0-py3-none-any.whl.
File metadata
- Download URL: readerkit-0.1.0-py3-none-any.whl
- Upload date:
- Size: 43.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.3 {"installer":{"name":"uv","version":"0.12.3","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d8ed1405946ba8dd557b48bafd3a1dbaeb1e04b6105e507d6beabd1249dba4f4
|
|
| MD5 |
613c4dd82482478a621043c0c7687cee
|
|
| BLAKE2b-256 |
e5481b396d6dac931b49725d9f914f7f4cf402168526779924dc580a360ec136
|