Skip to main content

ASGI static file server — like Whitenoise, but for ASGI

Project description

whitesnout

WhiteSnout is an ASGI static file server for Python — like Whitenoise, but built for ASGI frameworks (FastAPI, Starlette, Django, etc.). It serves static files with minimal memory overhead, streaming content in chunks and leveraging pre-compressed assets. A Rust extension (PyO3) accelerates the hot path transparently.


Quick start

from whitesnout import WhiteSnout

# Standalone static file server
app = WhiteSnout(directory="./static")

Serve with any ASGI server:

$ uvicorn myapp:app

With FastAPI

from fastapi import FastAPI
from whitesnout import WhiteSnout

api = FastAPI()

@api.get("/api")
def read_root():
    return {"hello": "world"}

app = WhiteSnout(api, directory="static")

With Django

# asgi.py
import os
from django.core.asgi import get_asgi_application
from whitesnout import WhiteSnout

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings")

django_app = get_asgi_application()
application = WhiteSnout(django_app, directory="static")

Installation

$ uv add whitesnout

Or with pip:

$ pip install whitesnout

Requires Python ≥ 3.10.

The compress CLI needs Brotli:

$ uv add 'whitesnout[compress]'

Pre-compressing assets

Generate .gz and .br variants for all files in a directory:

$ python -m whitesnout compress static/
Compressed: 42 gzip, 42 brotli

This is a build-time step — at runtime WhiteSnout serves the pre-compressed files directly with zero CPU overhead.


Configuration

All options can be passed as keyword arguments to WhiteSnout:

Option Default Description
app None Inner ASGI app to fall through to when a file is not found
directory "static" Root directory to serve files from
index_file "index.html" File to serve for directory requests
cache_max_age 3600 max-age in Cache-Control for regular files
immutable_max_age 31536000 max-age for hashed files (1 year)
immutable_pattern r"\.[a-f0-9]{8,}\." Regex to detect hashed filenames (e.g. styles.a1b2c3d4.css)
chunk_size 65536 Stream chunk size in bytes (64 KB)
charset "utf-8" Charset for text-based content types
brotli True Look for .br pre-compressed variants
gzip True Look for .gz pre-compressed variants
max_cache_size 100 Max entries in the LRU stat cache
app = WhiteSnout(
    app=my_asgi_app,
    directory="public",
    cache_max_age=86400,
    immutable_max_age=31536000,
    chunk_size=131072,
)

Features

  • ASGI-native — middleware or standalone, works with any ASGI framework
  • Streaming — files are served in configurable chunks (64 KB by default), never loaded entirely into memory
  • Zero-copy pre-compression — serves pre-existing .gz and .br files with automatic Accept-Encoding negotiation; brotli preferred over gzip
  • Powerful cachingETag, Last-Modified, Cache-Control headers; 304 Not Modified responses for conditional requests
  • Immutable cache — detects hashed filenames (e.g. app.abc12345.js) and applies Cache-Control: public, immutable, max-age=31536000
  • Index filesindex.html served automatically for directory paths
  • Clean URLs/dir redirects to /dir/ (301 Moved Permanently)
  • Path traversal protection — resolved paths are verified to stay within the root directory
  • Low overhead — LRU cache for file stats reduces stat() syscalls; no dependency bloat
  • MIME types — content-type detection for 30+ file extensions, with automatic charset for text types
  • Compress CLIpython -m whitesnout compress <directory> generates pre-compressed .gz and .br files as a build step
  • Rust extensionwhitesnout._rs speeds up the LRU cache transparently; pure Python fallback when unavailable
  • Multi-platform wheels — pre-built for Linux (x86_64, arm64), macOS (x86_64, arm64), and Windows (amd64)

Architecture

whitesnout/
├── main.py              # ASGI middleware (always Python)
├── file_handler.py      # Path resolution, compression negotiation
├── response.py          # Header building, chunked streaming, 304
├── cache.py             # LRU cache (Python → falls back to Rust)
├── config.py            # Configuration dataclass
├── utils.py             # MIME type table, helpers
├── cli.py               # CLI entry point
├── compress.py          # Compression logic
└── py.typed

whitesnout._rs           # Compiled Rust extension (PyO3)
├── LRUCache             # Rust implementation, auto fallback to Python

The core logic consists of pure functions designed for gradual migration to Rust. The ASGI integration layer (main.py) stays in Python forever — it is the thin touchpoint with the ASGI protocol.


Development

With Docker (recommended)

$ make build     # Build Docker image + compile Rust + install deps
$ make test      # Run test suite inside container
$ make shell     # Open interactive shell in container
$ make release   # Build release wheel

Requires Docker. The image is based on rust:slim-trixie with Python, uv, and maturin pre-installed.

Without Docker

$ uv sync --dev               # Install Python deps + build Rust extension
$ uv run pytest -v            # Run tests
$ maturin develop --uv        # Rebuild Rust extension only

Requires Rust (via rustup) and maturin (cargo install maturin).


CHANGELOG

See CHANGELOG.md for the full release history.


ROADMAP

v0.2.0 ─── Ruff + Ty + type safety (current)
v0.1.0 ─── Published
   │
   ├─ v0.2.0  Ruff + Ty + type safety
   ├─ v0.3.0  Range Requests + Security headers + Accept-Encoding quality values
   ├─ v0.4.0  Rust Phase 2 (utils, file_handler) + LRU cache O(1)
   ├─ v0.5.0  CORS + Logging + Cache invalidation
   └─ v1.0.0  Env vars + Async file IO + Benchmarks

v0.2.0 — Ruff + Ty

  • Add ruff (lint + format) and ty (type checker) for code validation
  • Fix all lint/type errors; pass both in CI

v0.3.0 — Range Requests & Security

  • Range Requests: Parse Range: header, respond with 206 Partial Content + Content-Range. Required for video, audio, and PDF seeking
  • Security headers: X-Content-Type-Options: nosniff, X-Frame-Options: DENY by default
  • Accept-Encoding quality values: Parse Accept-Encoding: gzip, br;q=0.1 correctly instead of naive substring matching
  • Respect brotli/gzip flags: find_compressed must skip .br when brotli=False

v0.4.0 — Rust Phase 2 + LRU O(1)

  • Port utils.py (MIME types) → src/utils.rs
  • Port file_handler.py (find_compressed, is_hashed_file) → src/file_handler.rs
  • Replace Vec-based Rust LRU with a LinkedHashMap for O(1) operations
  • Add Rust unit tests (#[cfg(test)])
  • Expand MIME type table from 30 → 100+

v0.5.0 — CORS, Logging & Cache invalidation

  • CORS opt-in: Config cors=True → add Access-Control-Allow-Origin: *
  • Logging: Basic request logging (method, path, status, bytes, duration)
  • Cache invalidation: Programmatic invalidate() method to purge the LRU cache

v1.0.0 — Production readiness

  • Environment variables: WHITESNOUT_DIRECTORY, WHITESNOUT_CACHE_MAX_AGE, etc.
  • Async file IO: Migrate iter_chunks to anyio or aiofiles to avoid blocking the event loop
  • Benchmarks: Compare against Whitenoise and raw ASGI serving

License

MIT — see LICENSE for the full text.

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

whitesnout-0.2.0.tar.gz (37.1 kB view details)

Uploaded Source

Built Distributions

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

whitesnout-0.2.0-cp310-abi3-win_amd64.whl (142.4 kB view details)

Uploaded CPython 3.10+Windows x86-64

whitesnout-0.2.0-cp310-abi3-musllinux_1_2_x86_64.whl (348.8 kB view details)

Uploaded CPython 3.10+musllinux: musl 1.2+ x86-64

whitesnout-0.2.0-cp310-abi3-musllinux_1_2_aarch64.whl (336.3 kB view details)

Uploaded CPython 3.10+musllinux: musl 1.2+ ARM64

whitesnout-0.2.0-cp310-abi3-manylinux_2_28_x86_64.whl (275.9 kB view details)

Uploaded CPython 3.10+manylinux: glibc 2.28+ x86-64

whitesnout-0.2.0-cp310-abi3-manylinux_2_28_aarch64.whl (269.9 kB view details)

Uploaded CPython 3.10+manylinux: glibc 2.28+ ARM64

whitesnout-0.2.0-cp310-abi3-macosx_11_0_arm64.whl (240.8 kB view details)

Uploaded CPython 3.10+macOS 11.0+ ARM64

whitesnout-0.2.0-cp310-abi3-macosx_10_12_x86_64.whl (244.6 kB view details)

Uploaded CPython 3.10+macOS 10.12+ x86-64

File details

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

File metadata

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

File hashes

Hashes for whitesnout-0.2.0.tar.gz
Algorithm Hash digest
SHA256 ab3985452bc91a4c8d5fd5bc3644c1fe303e3b4d5d3b4cc0be4e3c66bb7118f0
MD5 9d6cc1364cc91f6a89f0959bb9db14d5
BLAKE2b-256 5ac2c6e98122fac029e1e5ad3b85a1d7f02bcea9f7703b1de3361c8325236e37

See more details on using hashes here.

Provenance

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

Publisher: publish.yml on rroblf01/whitesnout

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

File details

Details for the file whitesnout-0.2.0-cp310-abi3-win_amd64.whl.

File metadata

  • Download URL: whitesnout-0.2.0-cp310-abi3-win_amd64.whl
  • Upload date:
  • Size: 142.4 kB
  • Tags: CPython 3.10+, Windows x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for whitesnout-0.2.0-cp310-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 905b1abb7821bea35368a8a48f591e64ab73832bc0608969bdde6d3475b25f20
MD5 c422ec19a13e0e548ce8fadd646c8b04
BLAKE2b-256 a1fe8958ebcffae90cc55b4a8d6275bd2c58488ae0f1bfba810478888f5877ef

See more details on using hashes here.

Provenance

The following attestation bundles were made for whitesnout-0.2.0-cp310-abi3-win_amd64.whl:

Publisher: publish.yml on rroblf01/whitesnout

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

File details

Details for the file whitesnout-0.2.0-cp310-abi3-musllinux_1_2_x86_64.whl.

File metadata

File hashes

Hashes for whitesnout-0.2.0-cp310-abi3-musllinux_1_2_x86_64.whl
Algorithm Hash digest
SHA256 3efd984fe3922b03200cc79306d42a8262951d685dc218b1c86f94240fb1b9f0
MD5 d606e5e253778a357bde2592d1a7905e
BLAKE2b-256 ac06a4bffba5c7eba04d52d5963bb7e7d314a0040e9d41035e6b9e428235e6ce

See more details on using hashes here.

Provenance

The following attestation bundles were made for whitesnout-0.2.0-cp310-abi3-musllinux_1_2_x86_64.whl:

Publisher: publish.yml on rroblf01/whitesnout

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

File details

Details for the file whitesnout-0.2.0-cp310-abi3-musllinux_1_2_aarch64.whl.

File metadata

File hashes

Hashes for whitesnout-0.2.0-cp310-abi3-musllinux_1_2_aarch64.whl
Algorithm Hash digest
SHA256 68b297777f43d694cbd44e2ca7d9e49a6f725ae42a33adef7a44974d959a99f3
MD5 7e5a688f391cbf3252664fb19513e291
BLAKE2b-256 712c254f14c372a992404b9d45b25d465800562d896ee2038be98c1264779da0

See more details on using hashes here.

Provenance

The following attestation bundles were made for whitesnout-0.2.0-cp310-abi3-musllinux_1_2_aarch64.whl:

Publisher: publish.yml on rroblf01/whitesnout

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

File details

Details for the file whitesnout-0.2.0-cp310-abi3-manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for whitesnout-0.2.0-cp310-abi3-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 1d1f530f4e51b3f57d56df8d6c381490a21996acf9a16a72e14d6da4d18c1a42
MD5 c321bb98dd53a6c0cd718fb259c5cdc1
BLAKE2b-256 42c7c46c1fede311b5d08061cb6802905f3c8da49da1e8312306265276228cd2

See more details on using hashes here.

Provenance

The following attestation bundles were made for whitesnout-0.2.0-cp310-abi3-manylinux_2_28_x86_64.whl:

Publisher: publish.yml on rroblf01/whitesnout

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

File details

Details for the file whitesnout-0.2.0-cp310-abi3-manylinux_2_28_aarch64.whl.

File metadata

File hashes

Hashes for whitesnout-0.2.0-cp310-abi3-manylinux_2_28_aarch64.whl
Algorithm Hash digest
SHA256 7c2f05d6f59dbec6268e389c8678918964229253662d784fe0ac4e1ab7131807
MD5 cfc43b830f9fecb0aab6a6d17caf35ac
BLAKE2b-256 2bd5891a2356dc79e6d29e51f7edf4bf73736f34585a9c6f41ed79b3dbb9f8df

See more details on using hashes here.

Provenance

The following attestation bundles were made for whitesnout-0.2.0-cp310-abi3-manylinux_2_28_aarch64.whl:

Publisher: publish.yml on rroblf01/whitesnout

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

File details

Details for the file whitesnout-0.2.0-cp310-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for whitesnout-0.2.0-cp310-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 9dae03a686ac2843934fe1972bb9d08192c4d6ade2dc40fa1c1cc5fc3e88f13e
MD5 d0ea351a7f869f902408c7d935a95df9
BLAKE2b-256 d754af77d376cbee6982d61625444be544c9bfdd34334ea654e9fb53a0002492

See more details on using hashes here.

Provenance

The following attestation bundles were made for whitesnout-0.2.0-cp310-abi3-macosx_11_0_arm64.whl:

Publisher: publish.yml on rroblf01/whitesnout

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

File details

Details for the file whitesnout-0.2.0-cp310-abi3-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for whitesnout-0.2.0-cp310-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 46bb299971189c11073fe8b5e8598213e4707198057df92f24598017b298b78d
MD5 565f3307ecbee7cc9d8f006e6d17245c
BLAKE2b-256 bed2ae17975000c803ee7d811033c7bcf40d7b1aed520654b6a123bb0c6b2747

See more details on using hashes here.

Provenance

The following attestation bundles were made for whitesnout-0.2.0-cp310-abi3-macosx_10_12_x86_64.whl:

Publisher: publish.yml on rroblf01/whitesnout

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