Skip to main content

Operator-curated, URL-keyed artifact cache for a small lab (CUDA/ROCm/DOCA/firmware)

Project description

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.

By default a miss is auto-fetched: the request falls through to origin (so the caller gets its file straight away), and the cache-host pulls the same artifact in the background, so the next request hits. Run with --curate to require a human instead, who reviews the miss list in a small web UI and presses Download (or pre-seeds via Add from URI). Either way the cache-host is the only box that needs internet egress (and any vendor credentials), and 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 no proxy offers the optional operator-curated model (--curate: a miss queue a human approves).

Components

Path What it is
src/withcache/server.py The cache-host: blob store + miss table + background download manager + operator UI (Pico.css + 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 and the Python shims are stdlib-only (no third-party runtime deps); the native shim is a dependency-free static binary.

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:3000/

Or without containers:

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

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, and --curate switches from auto-fetch to operator-approved pulls.

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:3000
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:3000
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:3000
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:3000
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:3000' \
  | 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:3000/ (Pico.css + HTMX, bundled offline) shows:

  • Misses: auto-fetched by default, or (under --curate) each with Download (queues a background pull) and Dismiss.
  • Downloads: live progress bars, queued/running/completed/cancelled/failed, Cancel, and Clear finished. Downloads run in a background worker pool, not in the request, so large pulls never block, modelled on bty's job managers.
  • Cached artifacts: URL, size, hits (times served) and misses (times requested before it was cached), SHA-256, fetched-at.
  • Add from URI: pre-seed an artifact before anyone misses it.

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)

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.

Tests

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

Project details


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.2.0.tar.gz (59.0 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.2.0-py3-none-musllinux_1_2_x86_64.whl (244.9 kB view details)

Uploaded Python 3musllinux: musl 1.2+ x86-64

withcache-0.2.0-py3-none-musllinux_1_2_aarch64.whl (262.6 kB view details)

Uploaded Python 3musllinux: musl 1.2+ ARM64

withcache-0.2.0-py3-none-manylinux_2_17_x86_64.whl (244.9 kB view details)

Uploaded Python 3manylinux: glibc 2.17+ x86-64

withcache-0.2.0-py3-none-manylinux_2_17_aarch64.whl (262.6 kB view details)

Uploaded Python 3manylinux: glibc 2.17+ ARM64

withcache-0.2.0-py3-none-any.whl (51.1 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for withcache-0.2.0.tar.gz
Algorithm Hash digest
SHA256 20cc5921c4efb42c4af6503ed0f0d243ca591acef0cea03e85ed0b4093cae745
MD5 27e61cb38c6006942eb6506888678a5a
BLAKE2b-256 6665905ce00698903d0eba5249a279f96e1f0d0f898cbdf83b90dd0f28613cb8

See more details on using hashes here.

Provenance

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

Publisher: release.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.2.0-py3-none-musllinux_1_2_x86_64.whl.

File metadata

File hashes

Hashes for withcache-0.2.0-py3-none-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 503a0dcc69be64f809addd58843b0880be6c7134bdf0a440646ef4640ab77646
MD5 6ea494101b36d475fe42f48728bd7362
BLAKE2b-256 6952c2e9c4bc86f99ebac45ac322067f7d97fd3852fc9d7861248ec18e9746a8

See more details on using hashes here.

Provenance

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

Publisher: release.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.2.0-py3-none-musllinux_1_2_aarch64.whl.

File metadata

File hashes

Hashes for withcache-0.2.0-py3-none-musllinux_1_2_aarch64.whl
Algorithm Hash digest
SHA256 b459671cb6dc507fca34a265688cbe1f58c30bf2a3f06ed65238e5415a47db59
MD5 e6df813792f51494146a4633a289b9cb
BLAKE2b-256 4d4311491129a393c52b9d392a1a42c052c2926f1c37612edf3f418bc99f23a9

See more details on using hashes here.

Provenance

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

Publisher: release.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.2.0-py3-none-manylinux_2_17_x86_64.whl.

File metadata

File hashes

Hashes for withcache-0.2.0-py3-none-manylinux_2_17_x86_64.whl
Algorithm Hash digest
SHA256 b195b8ff6d56bc0ae9b67a61c4f2ff2195218e0ae2ab706d584ef0b5f9517fd1
MD5 11977f6b3115779e5fcc870eba9d2d83
BLAKE2b-256 fc463ebf36ca6a6bdc8f678c2e687bd862073b77ea5bcd11b8aec35239d44b18

See more details on using hashes here.

Provenance

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

Publisher: release.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.2.0-py3-none-manylinux_2_17_aarch64.whl.

File metadata

File hashes

Hashes for withcache-0.2.0-py3-none-manylinux_2_17_aarch64.whl
Algorithm Hash digest
SHA256 1f593d16629140155f522cf5b0b0493c69834d832c4a9164068cf4ef38932baa
MD5 0d9d42c8fe1bbd7e78b120eef0fe3918
BLAKE2b-256 b6290327eeece968f9ea4de63e79b73736adc0942ffb8932b4cea5a4681fc4f5

See more details on using hashes here.

Provenance

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

Publisher: release.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.2.0-py3-none-any.whl.

File metadata

  • Download URL: withcache-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 51.1 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.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 0063a2cf2b0ddf3c39094520f414e1963f6c694d69a4ed4d9bf0b785bedbc407
MD5 9b73f928c41abfb72f3797ec0e5403dc
BLAKE2b-256 1192fc9c3ea4f303f600499b89f6403ee6ae8bd2de428724a9c8ad36fa16d497

See more details on using hashes here.

Provenance

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

Publisher: release.yml on safl/withcache

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

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page