Skip to main content

withcache

ci PyPI license built with Zig static musl

A tiny, operator-curated artifact cache for a small lab, for the big vendor downloads you re-pull constantly (CUDA, ROCm, DOCA, firmware, drivers), fronted by transparent curl/wget shims so existing scripts use it with no changes.

Think of it as "ccache for HTTP artifacts, without a proxy."

curl -fsSL https://the/origin/cuda.tar.gz -o cuda.tar.gz     # your script, unchanged
   └─ curlwithcache shim ─ WITHCACHE_SERVER set?
        ├─ cached  → served from the cache-host (fast, local)
        └─ miss/unset/unreachable → runs the real curl, exactly as written

Artifacts are cached by their origin URL as a key; the shim opts in by re-pointing the URL at the cache. No transparent proxy, no TLS interception, no client CA. The URL is a lookup key, not a connection target.

A miss falls through to origin (the caller gets its file straight away) and withcache records the miss on the operator's /ui/misses page. The operator reviews the miss list, picks what's worth caching, and one click Fetchs a URL into the catalog + downloads it. Every entry in the catalog is a byte-perfect copy of the origin at the time the operator hit Download; subsequent requests hit the local cache. The cache-host is the only box that needs internet egress (and any vendor credentials); clients never write to it.

Why not just curl + a caching proxy?

For https:// (i.e. every vendor download) a forward proxy can't cache without SSL-bump / MITM: curl tunnels TLS end-to-end via CONNECT, so the proxy only sees ciphertext. The shim sidesteps that entirely by re-pointing the URL to the cache instead of intercepting the connection. And a proxy that auto-fetches everything a client asks for isn't what you want in a lab; the operator-curated model here means only bytes the operator chose live on disk.

Components

Path What it is
src/withcache/server.py The cache-host: blob store + miss table + background download manager + events log
src/withcache/_app.py FastAPI app factory + operator UI wiring (Bootstrap 5 + Bootstrap Icons + HTMX)
src/withcache/_shim.py Shared shim core (find URL → probe → rewrite → exec)
src/withcache/curlwithcache.py / wgetwithcache.py The Python curl / wget shims
shim/shim.zig The native shim: one static binary, both tools via argv[0]
deploy/Containerfile, deploy/compose.yml Single Podman/Docker host deploy

The cache-host runs on FastAPI + Jinja + Bootstrap (matching bty + nbdmux for one trio-consistent chrome); the shims (Python and native) are dependency-free so they drop into any environment.

Install

The cache-host and Python shims (works on any box with Python):

pipx install withcache    # or: uv tool install withcache  /  pip install withcache
# provides: curlwithcache  wgetwithcache  withcache-server

The native shim (no Python needed, for minimal/distroless boxes; ~200 KB static musl binary). Grab it from the Releases page; one binary serves both tools by the name it's invoked as:

curl -L .../releases/.../withcache-shim-x86_64-linux-musl -o /usr/local/bin/curlwithcache
chmod +x /usr/local/bin/curlwithcache

The Python shim is also the tested oracle and install-time fallback for platforms without a prebuilt binary; a differential test asserts the binary and the Python plan() rewrite argv identically.

Deploy the cache-host

export WITHCACHE_ADMIN_PASSWORD=change-me    # protects the operator UI
podman compose -f deploy/compose.yml up -d   # or: docker compose -f ...
# operator UI:  http://withcache-server:8081/

Or without containers:

WITHCACHE_ADMIN_PASSWORD=change-me withcache-server --data-dir ./data --port 8081

Data (blobs + cache.db + session-secret) lives in the /data volume (or --data-dir). Artifacts are immutable per version, so there's no cache invalidation. --workers N sets the number of concurrent download workers, --max-bytes (e.g. 50G) caps the cache: when full it refuses new fills (no auto-eviction), and you free space by deleting artifacts in the UI.

Use the shims (transparent curl / wget)

Every approach is the same two ingredients: (1) point at the cache with WITHCACHE_SERVER, and (2) make curl/wget resolve to the shim. They differ only in how widely the system curl/wget is shadowed. Pick the least invasive one that fits.

Safety: with WITHCACHE_SERVER unset the shim is a pure pass-through (it just execs the real tool, zero network/parsing), so even the system-wide setup is harmless wherever the cache isn't configured. Worst case is always "no caching, curl still works."

These all use command -v curlwithcache, so they work whether you installed the native binary or the Python launcher (both land under that name).

1. No shadowing: call the shims by name (least invasive)

Nothing is renamed; you opt in per command. Good for trying it out or a script you can edit.

export WITHCACHE_SERVER=http://withcache-server:8081
curlwithcache -fsSL https://the/origin/cuda.tar.gz -o cuda.tar.gz
wgetwithcache https://the/origin/rocm.tar.gz

2. This shell only: shadow curl/wget for the session

Put curl/wget symlinks in a dir and prepend it to PATH in the current shell. Reversible by just closing the shell.

mkdir -p ~/.withcache/bin
ln -sf "$(command -v curlwithcache)" ~/.withcache/bin/curl
ln -sf "$(command -v wgetwithcache)" ~/.withcache/bin/wget

export WITHCACHE_SERVER=http://withcache-server:8081
export PATH="$HOME/.withcache/bin:$PATH"
hash -r                       # forget any cached curl/wget location

command -v curl               # -> ~/.withcache/bin/curl  (verify it's the shim)
curl -fsSL https://the/origin/cuda.tar.gz -o cuda.tar.gz   # existing scripts, unchanged
wget https://the/origin/rocm.tar.gz                        # still saved as rocm.tar.gz

3. Your user: make it the default for your shells (persistent)

Create the symlinks once, then add the two exports to your shell rc. Affects all your future interactive shells; undo by deleting the block.

mkdir -p ~/.withcache/bin
ln -sf "$(command -v curlwithcache)" ~/.withcache/bin/curl
ln -sf "$(command -v wgetwithcache)" ~/.withcache/bin/wget

cat >> ~/.bashrc <<'EOF'

# withcache: transparent curl/wget caching
export WITHCACHE_SERVER=http://withcache-server:8081
export PATH="$HOME/.withcache/bin:$PATH"
EOF

4. One project only: scope it with direnv

Drop an .envrc in a project tree (requires direnv); caching applies only inside that directory.

# .envrc
export WITHCACHE_SERVER=http://withcache-server:8081
PATH_add ~/.withcache/bin        # assumes the symlinks from approach 2/3 exist

Then direnv allow.

5. The whole machine: every user, every shell (most invasive)

Install the shim as curl/wget in /usr/local/bin (ahead of /usr/bin on the default PATH) and set the server globally. This also catches build tools and package managers that shell out to curl/wget.

sudo ln -sf "$(command -v curlwithcache)" /usr/local/bin/curl
sudo ln -sf "$(command -v wgetwithcache)" /usr/local/bin/wget

# A login-shell env file (covers interactive logins; daemons started outside a
# login shell won't see it; set WITHCACHE_SERVER in their unit if you need it).
echo 'export WITHCACHE_SERVER=http://withcache-server:8081' \
  | sudo tee /etc/profile.d/withcache.sh >/dev/null

On minimal/distroless hosts use the native shim binary here: same symlink, no Python required.

Verify / turn it off

command -v curl                       # which curl is in effect (the shim, or the real one)
export REAL_CURL=/usr/bin/curl        # optional: pin the wrapped tool (also $REAL_WGET)

unset WITHCACHE_SERVER                 # instantly back to plain curl (pass-through)
rm ~/.withcache/bin/curl ~/.withcache/bin/wget   # remove shadowing entirely

How it works: the shim scans for the URL, asks the cache, and execs the real tool:

  1. Find the real curl/wget on $PATH (skipping itself; $REAL_CURL/$REAL_WGET override).
  2. With WITHCACHE_SERVER set, find the URL (the scheme:// arg, or --url).
  3. Probe the cache with that same tool (curl -I / wget --spider).
    • Hit → re-point only the URL at http://server/b/<base64(origin)>/<basename> and exec the real tool (so -o, -O, -L, --retry, … all still apply, and the file is named after the artifact).
    • Miss / unreachableexec the real tool with your arguments untouched (origin); the miss is recorded for the operator.
  4. With no WITHCACHE_SERVER, it does zero network/parsing, just execs the real tool.

Notes & limits (all degrade gracefully; worst case is "no caching, curl still works"):

  • Needs the wrapped tool present (it shims it). Adds ~Python-startup latency per call.
  • URLs hidden in a -K/-i config file or piped via stdin aren't seen → those calls pass through uncached.
  • Per-tool env override: CURLWITHCACHE_SERVER / WGETWITHCACHE_SERVER beat WITHCACHE_SERVER.

Operator UI

http://withcache-server:8081/ (Bootstrap 5 + Bootstrap Icons + HTMX, bundled offline; matches bty's chrome for a consistent trio) is a five-page dashboard:

  • Dashboard (landing): catalog + cache + activity summary, health checklist, and the last N audit events.
  • Catalog: image catalog fetched from a nosi-style catalog.toml (URL configured on Settings > Catalog source). Add entries via the subnav's inline "Add ORAS" / "Add HTTPS" forms or "Fetch default" button, then Download each entry. Rows carry hits, size, download progress (live), and cached/failed pills.
  • Misses: URLs clients asked for that aren't downloaded yet. Each with Fetch (promotes to a catalog entry AND downloads it) and Dismiss (forget it).
  • Events: append-only audit log with a free-text filter and per-page pagination. Failure rows carry an ack button; the dashboard's Health tripwire flags unacknowledged failures.
  • Settings: identity + storage paths + catalog source (editable) + logging (uvicorn level) + auth.

Auth

Single-tenant session-cookie auth (modelled on bty's approach, env password instead of PAM). The read path (/blob, /b/…, /healthz) is open so shims never log in; the operator surface (/, /admin/*) is gated.

Env var Purpose
WITHCACHE_SERVER Cache-host URL the shims use
CURLWITHCACHE_SERVER / WGETWITHCACHE_SERVER Per-tool override of the above
WITHCACHE_ADMIN_PASSWORD Operator login password (unset ⇒ UI open, with a warning)
WITHCACHE_SESSION_SECRET Override the persisted cookie-signing key (optional)
WITHCACHE_CATALOG_URL Pin the image-catalog URL; env value beats the /admin/catalog_set_url override so a locked-down deploy stays locked (optional)

Cache keys & signed URLs

The key is scheme://host/path with the query string dropped by default, so CDN/presigned URLs (whose tokens change every request) still match by path. Pass --keep-query to the server for query-sensitive keys. Package-manager repos (.deb/.rpm) are GPG-signed and verified by the client regardless of transport, so caching them this way is safe.

Consume from another tool (the client library)

A tool that already knows its download URLs (e.g. an installer or a provisioner) can prefer the cache without shelling out to a shim or re-implementing the /b/ scheme. withcache.client is stdlib-only, so importing it adds no dependencies:

from withcache import client

# "use the cache when it's warm, the origin otherwise"
url = client.serve_url("http://cache:8081", origin) or origin

is_cached() is a graceful HEAD (a miss, timeout, or unreachable cache all return False, so you fall back to the origin). Since v0.10.0 a miss is recorded on the cache-host's /ui/misses page but no background fetch fires -- the operator explicitly chooses what to Download. The encoding is shared with the shims and server, so consumers stay in lockstep with the cache-host.

Pull an oras:// artifact (oras + client together)

For a registry blob, pair withcache.oras (resolve the reference to a blob URL

from withcache import client, oras

resolved = oras.resolve_ref("oras://ghcr.io/<owner>/<repo>@sha256:<digest>")
url, headers = resolved.blob_url, dict(resolved.headers)
url = client.serve_url(server, url, headers=headers) or url   # cache when warm

Tests

python -m unittest discover -s tests   # stdlib only, no test deps

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

withcache-0.13.3.tar.gz (482.6 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

withcache-0.13.3-py3-none-musllinux_1_2_x86_64.whl (652.2 kB view details)

Uploaded Python 3musllinux: musl 1.2+ x86-64

withcache-0.13.3-py3-none-musllinux_1_2_aarch64.whl (669.8 kB view details)

Uploaded Python 3musllinux: musl 1.2+ ARM64

withcache-0.13.3-py3-none-manylinux_2_17_x86_64.whl (652.2 kB view details)

Uploaded Python 3manylinux: glibc 2.17+ x86-64

withcache-0.13.3-py3-none-manylinux_2_17_aarch64.whl (669.9 kB view details)

Uploaded Python 3manylinux: glibc 2.17+ ARM64

withcache-0.13.3-py3-none-any.whl (458.3 kB view details)

Uploaded Python 3

File details

Details for the file withcache-0.13.3.tar.gz.

File metadata

  • Download URL: withcache-0.13.3.tar.gz
  • Upload date:
  • Size: 482.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for withcache-0.13.3.tar.gz
Algorithm Hash digest
SHA256 5b835d83060f8accb93e0a7d4f37d266c087205127e59e4d3777d64063f9ce45
MD5 dc5616695adf486303dec7aff72424f6
BLAKE2b-256 d5f993f23c9c1f473ce3db86db9b23af5e547e0cdfa0bb59957ac59d950821a6

See more details on using hashes here.

Provenance

The following attestation bundles were made for withcache-0.13.3.tar.gz:

Publisher: ci-cd.yml on safl/withcache

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file withcache-0.13.3-py3-none-musllinux_1_2_x86_64.whl.

File metadata

File hashes

Hashes for withcache-0.13.3-py3-none-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 3f216848cdfa5654d01ff001b7f8143fdbec50f88d3c44d2c01c3b15fbd0d969
MD5 fd433445040ce98fce857523ef4a0b62
BLAKE2b-256 99f7403ae886aa71afb0050358401b039d769157a13653918c1654111e4d0853

See more details on using hashes here.

Provenance

The following attestation bundles were made for withcache-0.13.3-py3-none-musllinux_1_2_x86_64.whl:

Publisher: ci-cd.yml on safl/withcache

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file withcache-0.13.3-py3-none-musllinux_1_2_aarch64.whl.

File metadata

File hashes

Hashes for withcache-0.13.3-py3-none-musllinux_1_2_aarch64.whl
Algorithm Hash digest
SHA256 b3186a637cf94551fe6ee28c52b79cfb6b911a35ae87d07839923f15497ddf9d
MD5 f4c49fa2d3967708da05cabe6d05ca49
BLAKE2b-256 851471eb2a06700533bd9cae3bbb5cd550f97b2510fac55c13743e08e15913f3

See more details on using hashes here.

Provenance

The following attestation bundles were made for withcache-0.13.3-py3-none-musllinux_1_2_aarch64.whl:

Publisher: ci-cd.yml on safl/withcache

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file withcache-0.13.3-py3-none-manylinux_2_17_x86_64.whl.

File metadata

File hashes

Hashes for withcache-0.13.3-py3-none-manylinux_2_17_x86_64.whl
Algorithm Hash digest
SHA256 7210e239951220a171af054e8af47d39c6012c4d3106a0b425b9b2a87de5b56c
MD5 d55741f83615b21af763408347be3f03
BLAKE2b-256 b96cb3777715d20c48b45dbcbeb253673772385ea5d8f95e7c4b30825bb2151c

See more details on using hashes here.

Provenance

The following attestation bundles were made for withcache-0.13.3-py3-none-manylinux_2_17_x86_64.whl:

Publisher: ci-cd.yml on safl/withcache

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file withcache-0.13.3-py3-none-manylinux_2_17_aarch64.whl.

File metadata

File hashes

Hashes for withcache-0.13.3-py3-none-manylinux_2_17_aarch64.whl
Algorithm Hash digest
SHA256 eb3c7067361c9b020efe8b79655bc6f4d260295a4be13f1c48cc010f96923d06
MD5 d08b37156cfff715c25764c6d5836ea4
BLAKE2b-256 ece4b97b1b539b12fffd5919dcbcbe4a02f485a1dd4e0fc6faf19e5a54442c1f

See more details on using hashes here.

Provenance

The following attestation bundles were made for withcache-0.13.3-py3-none-manylinux_2_17_aarch64.whl:

Publisher: ci-cd.yml on safl/withcache

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file withcache-0.13.3-py3-none-any.whl.

File metadata

  • Download URL: withcache-0.13.3-py3-none-any.whl
  • Upload date:
  • Size: 458.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for withcache-0.13.3-py3-none-any.whl
Algorithm Hash digest
SHA256 4a5bc2246de075d07a72c7d90938c2e8871939c1a43cb024ff9ad09caa984aea
MD5 c9adffac0c59ac3bde2a08b051f5c090
BLAKE2b-256 40b970b6258f0599799673cef58dd31f16f0f6a2261699c73bcbaf4395d82771

See more details on using hashes here.

Provenance

The following attestation bundles were made for withcache-0.13.3-py3-none-any.whl:

Publisher: ci-cd.yml on safl/withcache

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.13.3 This release

6 files

0.13.2

6 files

0.13.1

6 files

0.13.0

6 files

0.12.0

6 files

0.11.1

6 files

0.10.0

6 files

0.9.1

6 files

0.9.0

6 files

0.8.10

6 files

0.8.9

6 files

0.8.8

6 files

0.8.7

6 files

0.8.6

6 files

0.8.5

6 files

0.8.4

6 files

0.8.3

6 files

0.8.2

6 files

0.8.1

6 files

0.8.0

6 files

0.7.2

6 files

0.7.1

6 files

0.7.0

6 files

0.6.5

6 files

0.6.4

6 files

0.6.3

6 files

0.6.2

6 files

0.6.1

6 files

0.6.0

6 files

0.5.2

6 files

0.5.1

6 files

0.5.0

6 files

0.4.3

6 files

0.4.2

6 files

0.4.1

6 files

0.4.0

6 files

0.3.0

6 files

0.2.0

6 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page