Files and assets

A directory on disk, exposed at a URL prefix. Prefer Assets and Files from stario (also stario.filesystem). Use them for any tree you serve over HTTP: shipped CSS and JavaScript, images, uploads, generated files.

stario.staticassets (AssetManifest, StaticAssets) is still importable. It is obsolete. Do not use it in new code.

Build at import time. Call href() there. Call await attach(app) in bootstrap.

python
from stario import App, Assets, Files, Span
 
STATIC = Assets("./static", "/static")
UPLOADS = Files("./uploads", "/data")
STYLE_CSS = STATIC.href("css/app.css")
 
 
async def bootstrap(app: App, span: Span):
    span.attrs(await STATIC.attach(app))
    await UPLOADS.attach(app)
    yield

Which class

Assets is for files you ship with the app. href() hashes the file and pins that digest. The public URL includes the digest. A request for the logical path receives 307 to the hashed URL. Default cache policy is immutable for one year. The directory must exist when you construct Assets.

Files is for a live tree. href() checks that the path exists and returns the same relative path under the prefix (default /data). GET opens the current file. A new file on disk is served without another load(). Do not use Assets for visitor uploads: a pin is a snapshot.

Import either class from stario or stario.filesystem.

Construction

python
from pathlib import Path
 
from stario import Assets, Files
 
ASSETS = Assets(Path("static"), "/static")
MEDIA = Files(Path("uploads"), "/media")

url_prefix is a string. The prefix must not contain placeholders. A host on the prefix is valid for href() (CDN URLs). attach() and register() require an app-relative prefix such as /static or /data.

Hidden path segments (names starting with .) are skipped unless include_hidden=True. Symlinks are skipped unless follow_symlinks=True. Followed symlinks must stay inside the root.

Pass content_types={".webmanifest": "application/manifest+json"} to add or override MIME types. Keys are suffixes that start with a dot.

href

href(path) takes a relative POSIX path (css/app.css). It rejects .., a leading slash, backslashes, and empty segments. Optional query and fragment arguments append to the URL.

For Assets, the first href() of a path hashes the file (xxHash64) and pins size and mtime. A later href() of the same path returns the pin. If the file is missing, href() raises StarioError. If the file changes while hashing, it raises StarioRuntime.

For Files, href() resolves the path on each call. A missing file raises StarioError.

attach, register, load

await attach(app) registers GET and HEAD under {prefix}/{path...}, then loads the tree. It returns the same stats mapping as load().

register(app) and await load() stay available when you must split the work: tests, a host-only prefix for href(), or a walk without routes. attach() is the bootstrap call.

load() walks the directory once. It holds files that fit max_file_size (default 1 MiB) and the remaining max_bytes budget (default 64 MiB). Larger files stream from disk. Assets also hashes files that were not passed to href(). You can load only once.

After attach():

  • Files GET is live. A file written later is served.

  • The Assets catalog is the snapshot. GET of an unpinned new file is 404. href() after attach still pins that file and serves it.

cache_control= on attach() or register() overrides the class default (public, max-age=31536000, immutable for Assets; private, no-cache for Files).

HTTP

Both classes send X-Content-Type-Options: nosniff, a strong ETag, Last-Modified, and Accept-Ranges: bytes. Conditional GET returns 304. If-None-Match takes precedence over If-Modified-Since.

Assets ETags use the content digest (the same hash as the URL), distinct per content-coding. Files ETags use device, inode, mtime, and size.

Satisfiable Range: bytes=… returns 206 of the identity bytes (not a compressed representation). Unsatisfiable ranges return 416. If-Range is a strong comparison.

Logical Assets paths (no digest) receive 307 to the hashed URL. HTML should call href() so the page embeds the final URL. If a pinned Assets file changes on disk after the pin, GET of the hashed URL is 404 (not an immutable miss).

Pre-compression

Assets precompresses by default (br, zstd, gzip). Files does not, unless you pass precompress=. Pass precompress=() to disable, or a subset such as ("br", "gzip").

Variants are stored only when they are smaller than the identity body and the type is compressible. image/svg+xml is compressible. Other image/, audio/, video/, WOFF, and archive types are not. Cached replies negotiate Accept-Encoding and set Vary: Accept-Encoding when variants exist.

If the identity body fits the byte budget and a codec does not, the identity body stays in memory and that codec is skipped.

Pass compression=CompressionConfig(…) to tune levels and minimum size for this pass (defaults favor higher quality than live Writer compression). Streamed files are not precompressed.

stats

After load() or attach(), stats is a mapping: files, cached_files, streamed_files, budget_skipped_files, retained_bytes, compressed_files. Attach it to a bootstrap span when you want the cost in traces.

class Assets(directory, url_prefix=None, *, include_hidden=False, follow_symlinks=False, content_types=None)

Fingerprinted files. href() hashes and pins. GET uses the hashed URL.

async Assets.__call__(context, writer)

Call self as a function.

Assets.href(path, /, *, query=None, fragment=None)

class Files(directory, url_prefix=None, *, include_hidden=False, follow_symlinks=False, content_types=None)

Live files. href() checks the path exists. GET uses ETag and Range.

async Files.__call__(context, writer)

Call self as a function.

Files.href(path, /, *, query=None, fragment=None)

Obsolete: stario.staticassets

AssetManifest and StaticAssets remain in stario and stario.staticassets for existing apps. They are obsolete. New code should use Assets (and Files for live trees).

AssetManifest walks and fingerprints at construction. StaticAssets(manifest) reads and compresses at construction, then register(app) attaches routes. That split is replaced by one Assets object: href() at import, await attach(app) in bootstrap.

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 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 0x75825ec524a0>)

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:

python
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.