Static assets
Fingerprinted static files use a two-phase model: AssetManifest at import time for cheap URL building, and StaticAssets during bootstrap for serving, caching, and pre-compression.
Link generation and route registration are separate on purpose. Hashing every file is fast enough for module scope; reading, compressing, and wiring routes belongs in bootstrap where telemetry and teardown are available.
AssetManifest (import time)
AssetManifest(directory, …) walks a directory once, fingerprints each public file with xxHash64, and records logical paths, hashed filenames, and public URLs. No file contents are kept in memory.
Hidden path segments (names starting with .) are skipped unless include_hidden=True. Symlinked files are skipped unless follow_symlinks=True.
Each file’s public name is {stem}.{digest}{suffix} with relative directories preserved. Call manifest.href("css/app.css") to obtain the fingerprinted URL (prefix + hashed path). Optional query and fragment arguments append to that URL.
url_prefix may be a string or UrlPath. Host-prefixed manifests are valid for CDN URL generation; local serving requires an app-relative prefix (see below).
from pathlib import Path from stario import AssetManifest, UrlPath STATIC = UrlPath("/static")ASSETS = AssetManifest(Path("static"), url_prefix=STATIC) # In templates or handlers:ASSETS.href("js/app.js")class AssetManifest(directory='./static', *, url_prefix='/static', hash_chunk_size=4194304, include_hidden=False, follow_symlinks=False)
Scan a directory once: fingerprint every public file and map logical paths to public URLs.
Hashing only — no file contents are kept in memory — so a manifest is cheap enough to build at module level and use for href constants. Hand it to StaticAssets during bootstrap to actually serve the files. Hidden files and hidden directories are skipped by default; pass include_hidden=True when that is intentional. Symlinked files are also skipped unless follow_symlinks=True is passed; keep this false for static directories that should be self-contained.
AssetManifest.href(path, /, *, query=None, fragment=None)
Build the public fingerprinted URL for a logical asset path under this tree.
class Asset(logical_path, hashed_path, url, source, size, modified_ns)
One file in an AssetManifest.
Fields
- logical_path(str):Path relative to the manifest directory, e.g.
css/style.css. - hashed_path(str):Fingerprinted relative path, e.g.
css/style.abc123.css. - url(str):Public URL (prefix + hashed path), e.g.
/static/css/style.abc123.css. - source(Path):Absolute path to the file on disk.
- size(int):File size recorded when the manifest was built.
- modified_ns(int):Filesystem mtime (nanoseconds) recorded when the manifest was built.
StaticAssets (bootstrap)
StaticAssets(manifest, …) takes a manifest built earlier, verifies files still match recorded size and mtime, loads small files into memory (with optional pre-compressed variants), and prepares large files for streaming from disk. Construction is I/O heavy — build it in bootstrap, not at import time.
Call register(app) to attach GET and HEAD catch-all routes under manifest.prefix / "{path...}". Requests for non-fingerprinted logical paths receive 307 to the hashed URL. Immutable cache headers apply by default.
Host-prefixed manifests cannot be served locally; use an app-relative prefix such as UrlPath("/static") when calling register(app).
from collections.abc import AsyncIteratorfrom pathlib import Path from stario import App, AssetManifest, Span, StaticAssets, UrlPath STATIC = UrlPath("/static")ASSETS = AssetManifest(Path("static"), url_prefix=STATIC) async def bootstrap(app: App, span: Span) -> AsyncIterator[None]: with span.step("static_assets") as s: assets = StaticAssets(ASSETS) s.attrs(assets.stats) assets.register(app) yieldStaticAssets.href(path) delegates to the underlying manifest.
class StaticAssets(manifest, *, cache_control='public, max-age=31536000, immutable', cache_max_size=1048576, filesystem_chunk_size=65536, precompress=('br', 'zstd', 'gzip'), content_types=None, compression=<stario.http.compression.CompressionConfig object at 0x750585840b80>)
Serve an AssetManifest: cache small files (with pre-compression), stream large files.
Construction reads and compresses files, so build it during bootstrap — not at module level — then call register(app). Build URLs with href(path) on either the manifest or this serving wrapper. Non-fingerprint paths 307 to hashed URLs. Large streamed files support one Range: bytes=... request at a time.
Use precompress=() to disable startup compression, or choose an explicit subset such as precompress=("br",). Pass compression=CompressionConfig(...) to control levels, windows, and the minimum size. Use content_types={".webmanifest": "application/manifest+json"} for per-instance MIME overrides without mutating framework defaults.
Stario favors small fingerprinted assets: keep generated CSS/JS/images compact enough to cache and pre-compress at bootstrap. cache_max_size is an escape hatch for large files, which are streamed from disk and only support range requests in their uncompressed form.
stats summarizes what construction did (file counts, raw vs compressed bytes); attach it to a span if you want the cost in traces:
with span.step("static_assets") as s: assets = StaticAssets(ASSETS) s.attrs(assets.stats)assets.register(app)async StaticAssets.__call__(c, w)
GET/HEAD handler: resolve {path...} against the manifest, redirect, 404, or send bytes from memory or disk.
StaticAssets.href(path, /, *, query=None, fragment=None)
Build the public fingerprinted URL for a logical asset path.
StaticAssets.register(app)
Register GET/HEAD catch-all routes on the application.
Fingerprinting and redirects
Fingerprinting runs at manifest construction. If a file changes while the manifest is building, construction raises StarioError. StaticAssets re-checks size and mtime at bootstrap; drift after the manifest build also raises with a rebuild hint.
Browsers that request a logical path (without the digest) follow a 307 to the canonical fingerprinted URL. HTML should call href() so pages embed the final URL directly.
Pre-compression
By default precompress generates brotli, zstd, and gzip variants for eligible small files at bootstrap (default compression.min_size is 256 bytes; variants are stored only when smaller than raw). Pass precompress=() to disable, or a subset such as ("br",). Already-compressed types (images, video, font/woff, font/woff2, archive MIME types) skip recompression; other font formats (.ttf, .otf) may still be pre-compressed. Cached files negotiate Accept-Encoding per request and set Vary: Accept-Encoding when precompressed variants exist.
Pass compression=CompressionConfig(…) to tune levels and minimum size for the static pass (defaults favor higher quality than live Writer compression). Pass content_types={".webmanifest": "application/manifest+json"} for per-instance MIME overrides.
Large files (above cache_max_size, default 1 MiB) stay on disk: metadata only in memory, streamed on demand without holding full precompressed blobs.
Range requests
Large streamed files support a single Range: bytes=… request per exchange. Satisfiable ranges return 206 Partial Content; unsatisfiable ranges return 416. Range handling applies to the uncompressed entity on the streaming path.
stats
After construction, assets.stats is a read-only mapping summarizing file counts, raw bytes, cached vs streamed files, and compressed variant sizes. Attach it to a bootstrap span step when you want build cost visible in traces (see example above).