Classes
_RangeNotSatisfiable
0
▼
Sentinel for a valid but unsatisfiable Range request (-> 416).
_RangeNotSatisfiable
0
▼
Enum
Sentinel for a valid but unsatisfiable Range request (-> 416).
StaticMount
9
▼
Configuration for a static file mount point.
StaticMount
9
▼
Configuration for a static file mount point.
Attributes
| Name | Type | Description |
|---|---|---|
url_path |
str
|
— |
directory |
Path
|
— |
cache_control |
str
|
— |
precompressed |
bool
|
— |
follow_symlinks |
bool
|
— |
index_file |
str | None
|
— |
extra_mime_types |
dict[str, str]
|
— |
stat_cache |
bool
|
— |
stat_cache_size |
int
|
— |
StaticFile
8
▼
Resolved static file with metadata.
StaticFile
8
▼
Resolved static file with metadata.
Attributes
| Name | Type | Description |
|---|---|---|
path |
Path
|
— |
size |
int
|
— |
mtime |
float
|
— |
mime_type |
str
|
— |
etag |
str
|
— |
encoding |
str | None
|
— |
cache_control |
str
|
— |
vary |
bool
|
— |
StaticFiles
24
▼
ASGI-compatible static file handler.
Can be used as middleware or integrated into Worker.
Example…
StaticFiles
24
▼
ASGI-compatible static file handler.
Can be used as middleware or integrated into Worker.
Example as middleware:
from pounce import StaticFiles
app = StaticFiles(
app,
mounts=[
StaticMount("/static", Path("./public")),
StaticMount("/assets", Path("./dist")),
]
)
Example in Worker (built-in): config = ServerConfig(static_files={"/static": "./public"})
Methods
Internal Methods 24 ▼
__init__
2
▼
Initialize static file handler.
__init__
2
▼
def __init__(self, app: ASGIApp | None = None, *, mounts: list[StaticMount]) -> None
Parameters
| Name | Type | Description |
|---|---|---|
app |
— |
Optional ASGI app to call if path doesn't match (middleware mode) Default:None
|
mounts |
— |
List of static mount configurations |
Normalize and validate mount configurations.
def _prepare_mounts(self, mounts: list[StaticMount]) -> list[StaticMount]
Parameters
| Name | Type | Description |
|---|---|---|
mounts |
— |
__call__
3
▼
Handle ASGI request.
If path matches a static mount, serve the file. Otherwise…
async
__call__
3
▼
async def __call__(self, scope: dict[str, Any], receive: Receive, send: Send) -> None
Handle ASGI request.
If path matches a static mount, serve the file. Otherwise, call the app.
Parameters
| Name | Type | Description |
|---|---|---|
scope |
— |
|
receive |
— |
|
send |
— |
Resolve URL path to static file, using the opt-in stat cache.
When the cache i…
def _resolve_file(self, url_path: str, accept_encoding: bytes | None) -> StaticFile | None
Resolve URL path to static file, using the opt-in stat cache.
When the cache is enabled (at least one mount sets stat_cache) a
repeat request revalidates the cached entry with a singlestat()
on the already-resolved path; if mtime and size are unchanged it is
returned directly, skippingresolve(), lstat(), and the
precompressed probes. A changed mtime/size (e.g. a Bengal rebuild) or
a now-missing file invalidates the entry and falls through to a full,
security-checked re-resolution.
Parameters
| Name | Type | Description |
|---|---|---|
url_path |
— |
|
accept_encoding |
— |
_mount_caches
1
bool
▼
Whether the mount serving ``url_path`` opts into the stat cache.
_mount_caches
1
bool
▼
def _mount_caches(self, url_path: str) -> bool
Parameters
| Name | Type | Description |
|---|---|---|
url_path |
— |
Returns
bool
Resolve URL path to static file (full security-checked path).
def _resolve_file_uncached(self, url_path: str, accept_encoding: bytes | None) -> StaticFile | None
Parameters
| Name | Type | Description |
|---|---|---|
url_path |
— |
|
accept_encoding |
— |
_find_precompressed
4
tuple[Path, str | None]
▼
Find precompressed variant if available and client supports.
Priority: zstd > …
_find_precompressed
4
tuple[Path, str | None]
▼
def _find_precompressed(self, path: Path, mount: StaticMount, accept_encoding: bytes | None, original_stat: os.stat_result) -> tuple[Path, str | None]
Find precompressed variant if available and client supports.
Priority: zstd > gzip > identity
Parameters
| Name | Type | Description |
|---|---|---|
path |
— |
|
mount |
— |
|
accept_encoding |
— |
|
original_stat |
— |
Returns
tuple[Path, str | None]
(file_path, encoding) where encoding is "gzip", "zstd", or None
_validate_precompressed
2
bool
▼
Validate a precompressed variant against the same security checks as the origin…
_validate_precompressed
2
bool
▼
def _validate_precompressed(self, path: Path, mount: StaticMount) -> bool
Validate a precompressed variant against the same security checks as the original.
Checks path traversal, hidden files, and symlinks.
Parameters
| Name | Type | Description |
|---|---|---|
path |
— |
|
mount |
— |
Returns
bool
True if the precompressed path is safe to serve
_get_mime_type
2
str
▼
Get MIME type for file.
**Lookup order:**
1. Mount-specific extra_mime_types (…
_get_mime_type
2
str
▼
def _get_mime_type(self, path: Path, mount: StaticMount | None = None) -> str
Get MIME type for file.
Lookup order:
- Mount-specific extra_mime_types (user overrides)
- stdlib mimetypes.guess_type()
- _MODERN_MIME_TYPES fallback for modern web extensions
- application/octet-stream
Parameters
| Name | Type | Description |
|---|---|---|
path |
— |
|
mount |
— |
Default:None
|
Returns
str
MIME type string
_generate_etag
3
str
▼
Generate ETag from mtime, size, and encoding.
Uses weak ETag (W/) because we u…
_generate_etag
3
str
▼
def _generate_etag(self, mtime: float, size: int, encoding: str | None = None) -> str
Generate ETag from mtime, size, and encoding.
Uses weak ETag (W/) because we use mtime, not content hash. Encoding is included so that compressed and uncompressed variants of the same file produce distinct ETags (RFC 7232).
Parameters
| Name | Type | Description |
|---|---|---|
mtime |
— |
|
size |
— |
|
encoding |
— |
Default:None
|
Returns
str
ETag header value (e.g., W/"5f3c-1a2b" or W/"5f3c-1a2b-gzip")
_check_not_modified
2
bool
▼
Check if client has cached version (If-None-Match).
_check_not_modified
2
bool
▼
def _check_not_modified(self, headers: list[tuple[bytes, bytes]], file: StaticFile) -> bool
Parameters
| Name | Type | Description |
|---|---|---|
headers |
— |
|
file |
— |
Returns
bool
True if client cache is valid (send 304), False otherwise
_modified_since
2
bool
▼
Whether the file was modified after the supplied instant.
Compares whole secon…
staticmethod
_modified_since
2
bool
▼
def _modified_since(file: StaticFile, since: datetime) -> bool
Whether the file was modified after the supplied instant.
Compares whole seconds: HTTP-dates have one-second resolution, while a filesystem mtime may carry sub-second precision. Truncating both to seconds avoids spurious "modified" results from microsecond drift.
Parameters
| Name | Type | Description |
|---|---|---|
file |
— |
|
since |
— |
Returns
bool
_if_range_matches
2
bool
▼
Whether an If-Range value matches the current representation.
A range request …
_if_range_matches
2
bool
▼
def _if_range_matches(self, if_range: bytes, file: StaticFile) -> bool
Whether an If-Range value matches the current representation.
A range request guarded by If-Range serves 206 only on a match; on a mismatch the full 200 entity is returned (RFC 9110 §13.1.5).
Pounce emits only weak ETags, which MUST NOT be used in an If-Range comparison. So an ETag-valued If-Range (anything quoted or weak-prefixed) can never match -> full 200. A date-valued If-Range matches only when the file has not been modified since that date.
Parameters
| Name | Type | Description |
|---|---|---|
if_range |
— |
|
file |
— |
Returns
bool
_parse_range_header
2
list[tuple[int, int]] | …
▼
Parse Range header and return list of (start, end) byte ranges.
Format: "bytes…
_parse_range_header
2
list[tuple[int, int]] | …
▼
def _parse_range_header(self, range_header: str, file_size: int) -> list[tuple[int, int]] | None | _RangeNotSatisfiable
Parse Range header and return list of (start, end) byte ranges.
Format: "bytes=0-499" or "bytes=500-999" or "bytes=-500"
Distinguishes three outcomes (RFC 7233):
- Malformed Range (bad syntax,
start > end) ->None; the caller ignores the header and serves a full 200 response. - Valid but unsatisfiable (an explicit
start >= file_size) ->_RANGE_NOT_SATISFIABLE; the caller sends 416. - Satisfiable -> a coalesced list of (start, end) tuples. An explicit
endpast EOF is clamped tofile_size - 1rather than rejected. Requests with more than_MAX_RANGESparts are treated as abusive and ignored (None-> full 200).
Parameters
| Name | Type | Description |
|---|---|---|
range_header |
— |
|
file_size |
— |
Returns
list[tuple[int, int]] | None | _RangeNotSatisfiable
List of (start, end) tuples (inclusive), ``None`` if the header
should be ignored, or the unsatisfiable sentinel.
_coalesce_ranges
1
list[tuple[int, int]]
▼
Merge overlapping or adjacent byte ranges.
Sorting then merging bounds the wor…
staticmethod
_coalesce_ranges
1
list[tuple[int, int]]
▼
def _coalesce_ranges(ranges: list[tuple[int, int]]) -> list[tuple[int, int]]
Merge overlapping or adjacent byte ranges.
Sorting then merging bounds the work (and response size) of a multi-range request to the size of the file itself.
Parameters
| Name | Type | Description |
|---|---|---|
ranges |
— |
Returns
list[tuple[int, int]]
Coalesced ranges sorted by start offset.
_get_header
2
bytes | None
▼
Get header value by name (case-insensitive).
_get_header
2
bytes | None
▼
def _get_header(self, headers: list[tuple[bytes, bytes]], name: bytes) -> bytes | None
Parameters
| Name | Type | Description |
|---|---|---|
headers |
— |
|
name |
— |
Returns
bytes | None
Header value as bytes, or None if not found
_send_304
2
▼
Send 304 Not Modified response.
async
_send_304
2
▼
async def _send_304(self, file: StaticFile, send: Send) -> None
Parameters
| Name | Type | Description |
|---|---|---|
file |
— |
|
send |
— |
_send_416
2
▼
Send 416 Range Not Satisfiable (RFC 7233 §4.4).
Includes ``Content-Range: byte…
async
_send_416
2
▼
async def _send_416(self, file: StaticFile, send: Send) -> None
Send 416 Range Not Satisfiable (RFC 7233 §4.4).
IncludesContent-Range: bytes */<size>so the client learns the
current representation length, plus ETag and Accept-Ranges.
Parameters
| Name | Type | Description |
|---|---|---|
file |
— |
|
send |
— |
_send_206
4
▼
Send 206 Partial Content response (RFC 7233).
Single range: Content-Range head…
async
_send_206
4
▼
async def _send_206(self, file: StaticFile, ranges: list[tuple[int, int]], send: Send, *, sendfile_enabled: bool = False) -> None
Send 206 Partial Content response (RFC 7233).
Single range: Content-Range header with the range body. Multiple ranges: multipart/byteranges body with MIME boundary.
Parameters
| Name | Type | Description |
|---|---|---|
file |
— |
|
ranges |
— |
|
send |
— |
|
sendfile_enabled |
— |
Default:False
|
_send_206_single
4
▼
Send a single-range 206 response.
async
_send_206_single
4
▼
async def _send_206_single(self, file: StaticFile, range_pair: tuple[int, int], send: Send, *, sendfile_enabled: bool = False) -> None
Parameters
| Name | Type | Description |
|---|---|---|
file |
— |
|
range_pair |
— |
|
send |
— |
|
sendfile_enabled |
— |
Default:False
|
_send_206_multipart
3
▼
Send a multipart/byteranges 206 response (RFC 7233 §4.1).
Each part has its ow…
async
_send_206_multipart
3
▼
async def _send_206_multipart(self, file: StaticFile, ranges: list[tuple[int, int]], send: Send) -> None
Send a multipart/byteranges 206 response (RFC 7233 §4.1).
Each part has its own Content-Type and Content-Range headers, separated by a MIME boundary. Sendfile is not used here because the part headers must be interleaved with file data.
Parameters
| Name | Type | Description |
|---|---|---|
file |
— |
|
ranges |
— |
|
send |
— |
_send_file
4
▼
Send full file response (200 OK).
async
_send_file
4
▼
async def _send_file(self, file: StaticFile, method: str, send: Send, *, sendfile_enabled: bool = False) -> None
Parameters
| Name | Type | Description |
|---|---|---|
file |
— |
|
method |
— |
|
send |
— |
|
sendfile_enabled |
— |
Default:False
|
_send_file_body
5
▼
Send file body, using protocol-owned zero-copy sendfile when available.
When t…
async
_send_file_body
5
▼
async def _send_file_body(self, path: Path, offset: int, count: int, send: Send, *, sendfile_enabled: bool = False) -> None
Send file body, using protocol-owned zero-copy sendfile when available.
When the ASGI scope advertisespounce.sendfile, emit a Pounce
extension message describing the file range. The bridge and active
protocol own framing, byte accounting, and socket writes.
Falls back to chunked reads through ASGI send otherwise.
Tiny bodies (count < _SENDFILE_MIN_SIZE) always take the
read()+write() fallback even when sendfile is advertised: the
per-response transport detach/re-attach overhead outweighs the
zero-copy benefit below that measured threshold (issue #127).
Parameters
| Name | Type | Description |
|---|---|---|
path |
— |
|
offset |
— |
|
count |
— |
|
send |
— |
|
sendfile_enabled |
— |
Default:False
|
_send_file_range
5
▼
Send file range (for 206 responses).
async
_send_file_range
5
▼
async def _send_file_range(self, path: Path, start: int, count: int, send: Send, *, sendfile_enabled: bool = False) -> None
Parameters
| Name | Type | Description |
|---|---|---|
path |
— |
|
start |
— |
|
count |
— |
|
send |
— |
|
sendfile_enabled |
— |
Default:False
|
Functions
_http_date
1
str
▼
Format an mtime (epoch seconds) as an RFC 9110 IMF-fixdate (GMT).
_http_date
1
str
▼
def _http_date(mtime: float) -> str
Parameters
| Name | Type | Description |
|---|---|---|
mtime |
float |
Returns
str
_parse_http_date
1
datetime | None
▼
Parse an HTTP-date header value to a timezone-aware UTC datetime.
Returns ``No…
_parse_http_date
1
datetime | None
▼
def _parse_http_date(value: bytes) -> datetime | None
Parse an HTTP-date header value to a timezone-aware UTC datetime.
ReturnsNonefor unparseable input so callers can ignore a bad
conditional header and serve the full response (RFC 9110 §13.1.3).
Parameters
| Name | Type | Description |
|---|---|---|
value |
bytes |
Returns
datetime | None
Create StaticFiles handler from simple dict config.
def create_static_handler(mounts: dict[str, str], cache_control: str = 'public, max-age=3600', precompressed: bool = True, follow_symlinks: bool = False, index_file: str | None = 'index.html') -> StaticFiles
Parameters
| Name | Type | Description |
|---|---|---|
mounts |
dict[str, str] |
Dict of {url_path: directory} mappings |
cache_control |
str |
Cache-Control header value Default:'public, max-age=3600'
|
precompressed |
bool |
Serve .gz/.zst if available Default:True
|
follow_symlinks |
bool |
Allow serving symlinked files Default:False
|
index_file |
str | None |
Filename to serve for directories Default:'index.html'
|
Returns
StaticFiles