Helper classes to read files over HTTP using Range requests, with caching
Project description
hctef
Python library with helper classes to read files over HTTP using Range requests, with caching.
Overview
hctef provides a file-like interface for reading files over HTTP/HTTPS, using
HTTP Range requests to fetch only the data you need. It includes intelligent
caching to minimize network requests and supports both synchronous and
asynchronous operations.
Features
- File-like API: Works like a regular Python file object with
read(),seek(), andtell()methods - Efficient Range Requests: Fetches only the data you need using HTTP Range headers
- Disk-backed block cache: Caches fixed-size blocks on disk and leans on the OS page cache for the in-memory tier, so no file data is held in Python beyond transient buffers. The cache persists across processes and survives restarts
- Prefetching: Optionally prefetch data from the start or end of the file
- Sync and Async: Both synchronous and asynchronous implementations available
- Context Manager Support: Use with
withstatements for automatic cleanup
Installation
pip install hctef
To include async support:
pip install hctef[async]
The [async] extra pulls in aiohttp, the default async transport on regular
(CPython) runtimes. When running under Pyodide
(Python-in-the-browser via WebAssembly), no extra is needed: AsyncHttpFile
automatically uses pyodide.http.pyfetch (a thin wrapper over the browser's
fetch API that ships with the Pyodide runtime) instead, since socket-based
clients like aiohttp cannot work in the browser.
Quick Start
Synchronous Usage
from hctef import HttpFile
url = "https://example.com/large-file.bin"
with HttpFile(url) as f:
# Read first 100 bytes
data = f.read(100)
# Seek to a specific position
f.seek(1000)
# Read from current position
more_data = f.read(50)
# Get current position
position = f.tell()
# Seek relative to end of file
f.seek(-100, 2)
Asynchronous Usage
The async implementation supports independent cursors for concurrent reads:
import asyncio
from hctef.aio import AsyncHttpFile
url = "https://example.com/large-file.bin"
async with AsyncHttpFile(url) as f:
# Read first 100 bytes
data = await f.read(100)
# Seek to a specific position (synchronous - no I/O)
f.seek(1000)
# Read from current position
more_data = await f.read(50)
Parallel Reads with Multiple Cursors
Create independent cursors to read from different positions concurrently:
import asyncio
from hctef.aio import AsyncHttpFile
url = "https://example.com/large-file.bin"
async with AsyncHttpFile(url) as f:
# Create independent cursors for parallel reading
cursor1 = f.clone()
cursor2 = f.clone()
# Position each cursor at different locations
f.seek(0)
cursor1.seek(1000)
cursor2.seek(2000)
# Read from all three positions in parallel
# All cursors share the same cache and HTTP session
results = await asyncio.gather(
f.read(100), # Read bytes 0-100
cursor1.read(100), # Read bytes 1000-1100
cursor2.read(100), # Read bytes 2000-2100
)
# Each cursor maintains independent position
print(f.tell()) # 100
print(cursor1.tell()) # 1100
print(cursor2.tell()) # 2100
Cursors are lightweight and share:
- HTTP session (connection pooling)
- Byte range cache (deduplication of overlapping requests)
- File metadata
Async Transports
AsyncHttpFile supports two HTTP transports, selected via the transport
parameter:
'aiohttp'(default on CPython): uses anaiohttp.ClientSession. Requires the[async]extra. Thesession_kwargsparameter is passed through toaiohttp.ClientSessionand is specific to this transport.'pyfetch'(default under Pyodide/emscripten): usespyodide.http.pyfetch, i.e. the browser'sfetchAPI. Requires no extra dependencies but only works inside a Pyodide runtime.
When transport is not given, the transport is chosen automatically based on
the runtime (sys.platform == 'emscripten' selects 'pyfetch'):
# Force a specific transport
f = AsyncHttpFile(url, transport='pyfetch')
Custom transports
transport also accepts a transport instance, used as-is. AsyncTransport
is a structural protocol (typing.Protocol), so any class with the three
methods works — no hctef imports or subclassing required:
class MyTransport:
async def probe(self, url: str) -> RemoteFileInfo:
"""Return an object with .size, .etag, and .last_modified."""
async def fetch_range(self, url: str, start: int, end: int) -> bytes:
"""Return the byte range [start, end) — end-exclusive."""
async def close(self) -> None:
"""Release any resources held by the transport."""
transport = MyTransport()
async with AsyncHttpFile(url, transport=transport) as f:
data = await f.read(1024)
await transport.close() # you own it; hctef won't close it for you
Ownership rule: transports created by AsyncHttpFile from a name
('aiohttp'/'pyfetch'/auto-selected) are owned by it and closed on
close() (and on open failure). An injected instance is never closed by
AsyncHttpFile — the caller manages its lifecycle, which also makes it safe
to share one transport across several files. session_kwargs only configures
the built-in 'aiohttp' backend; combining it with an injected instance (or
'pyfetch') raises ValueError.
CORS note for the browser: with the
'pyfetch'transport, response headers on cross-origin requests are only visible when the server exposes them. If opening a file fails with a "cannot determine file size" error, the server likely needs to sendAccess-Control-Expose-Headers: Content-Rangein addition to allowing the origin.
Configuration Options
Both HttpFile and AsyncHttpFile accept the following parameters:
HttpFile(
url,
prefetch_bytes=1048576, # Bytes to prefetch on open (default: 1 MiB)
prefetch_direction='END', # 'START' or 'END' (default: 'END')
cache_dir=None, # Where to store the block cache (default: temp dir)
block_size=None, # Fixed block size in bytes (default: 1 MiB)
max_bytes=None, # Optional cap on the whole cache dir (LRU eviction)
immutable=None, # Skip etag/last-modified validation
)
prefetch_bytes: How many bytes to fetch immediately when opening the file. Set to 0 to disable prefetchingprefetch_direction: Whether to prefetch from the start ('START') or end ('END') of the filecache_dir: Directory holding the disk block cache. When omitted, a per-process temporary directory is created and removed on closeblock_size: Fixed cache block size. All reads are serviced by fetching and storing whole blocks; this subsumes the old request-coalescing knobmax_bytes: Optional size cap over the entirecache_dir. When exceeded, least-recently-used blocks are evicted at write timeimmutable: Trust an existing cache without revalidatingETag/Last-Modifiedagainst the live response
AsyncHttpFile additionally accepts:
transport: Which async HTTP transport to use,'aiohttp'or'pyfetch'(see Async Transports). Defaults to'pyfetch'under Pyodide/emscripten and'aiohttp'everywhere else. May also be anAsyncTransportinstance, which is used as-is and never closed byAsyncHttpFile— the caller owns its lifecyclesession_kwargs: Keyword arguments foraiohttp.ClientSession; specific to the built-in'aiohttp'transport (raisesValueErrorwhen combined with'pyfetch'or an injected transport instance)
Note:
minimum_range_request_bytesis deprecated and ignored (it emits aDeprecationWarning);block_sizereplaces it.
Environment variables
When the corresponding constructor argument is not given, configuration falls back to these environment variables:
| Variable | Meaning |
|---|---|
HCTEF_CACHE_DIR |
Cache directory (else a temp dir is used) |
HCTEF_CACHE_BLOCK_BYTES |
Block size in bytes (default 1 MiB) |
HCTEF_CACHE_MAX_BYTES |
Cap for the whole cache dir (default: unbounded) |
HCTEF_CACHE_IMMUTABLE |
Truthy value (1/true/yes/on) to skip validation |
Precedence is: explicit constructor argument, then environment variable, then the built-in default.
tmpfs caveat: On Linux the default temporary directory (
/tmp) is often atmpfsmount backed by RAM. In that case the "disk" block cache actually lives in memory, defeating the goal of keeping bytes out of RAM. Setcache_dir/HCTEF_CACHE_DIRto a path on real disk when that matters.
Requirements
- Python 3.12 or higher
- HTTP server must support Range requests
- For async on CPython:
aiohttp>=3.13.0(the[async]extra) - For async in the browser: the Pyodide runtime (provides
pyodide.http.pyfetch; no extra packages needed)
How It Works
When you open an HTTP file, hctef:
- Sends an initial Range request to determine the file size and verify Range
support, capturing
ETag/Last-Modifiedvalidators - Opens (or validates and, on mismatch, wipes) a per-URL directory under the
cache dir, keyed by
sha256(url), holding ameta.jsonand one file per fixed-size block - Optionally prefetches data from the start or end of the file
- On
read(), maps the request to a block range, fetches only the missing blocks (coalescing contiguous gaps into single Range requests), writes each block atomically, and assembles the result by reading the block files
Because blocks live on disk and are read back through the OS page cache, hot data stays fast without being pinned in Python memory, and the cache is reused by later opens and other processes sharing the same directory.
Error Handling
hctef defines custom exceptions:
HctefError: Base exception classHctefNetworkError: Raised for network-related errors (inherits fromIOError)HctefUrlError: Raised for invalid URLs (inherits fromValueError)
from hctef import HttpFile
from hctef.exceptions import HctefNetworkError, HctefUrlError
try:
with HttpFile("https://example.com/file.bin") as f:
data = f.read(100)
except HctefNetworkError as e:
print(f"Network error: {e}")
except HctefUrlError as e:
print(f"Invalid URL: {e}")
Development
To set up for development:
# Clone the repository
git clone https://github.com/jkeifer/hctef
cd hctef
# Install dependencies
uv sync --all-extras --dev
# Setup git hooks (prek, a fast pre-commit reimplementation)
uv run prek install
# Run tests
uv run pytest
# Run all checks with prek
uv run prek run --all-files
Future Ideas
- Allow uncached "cursor" for reading a large file segment
- Optional integrity checks on cached blocks
License
Apache License 2.0
What is hctef?
It's the HTTP Client That Eats Files, obviously.
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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file hctef-0.3.1.tar.gz.
File metadata
- Download URL: hctef-0.3.1.tar.gz
- Upload date:
- Size: 87.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6cff8a500c9669d0718cef2a7de7269a5bbf43ede8993e92986522ba0e6c02c8
|
|
| MD5 |
753ce3adfe3f5ec0064f480b678f8edb
|
|
| BLAKE2b-256 |
98875883f4c2ad8afcaa688660c0f2c4e63c7f2109fa82cb5b866f8c87022698
|
Provenance
The following attestation bundles were made for hctef-0.3.1.tar.gz:
Publisher:
release.yml on jkeifer/hctef
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
hctef-0.3.1.tar.gz -
Subject digest:
6cff8a500c9669d0718cef2a7de7269a5bbf43ede8993e92986522ba0e6c02c8 - Sigstore transparency entry: 2083874759
- Sigstore integration time:
-
Permalink:
jkeifer/hctef@bcee35a6ec937c621eed78a61eea47ea334fdf79 -
Branch / Tag:
refs/tags/v0.3.1 - Owner: https://github.com/jkeifer
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@bcee35a6ec937c621eed78a61eea47ea334fdf79 -
Trigger Event:
release
-
Statement type:
File details
Details for the file hctef-0.3.1-py3-none-any.whl.
File metadata
- Download URL: hctef-0.3.1-py3-none-any.whl
- Upload date:
- Size: 24.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
08ded822bc850b7be5f3e3fa8ed380818c9a3f4ad3c70ee9f93b9057c01290ac
|
|
| MD5 |
2f688fdd70fa9cbd44e41a0dec31df13
|
|
| BLAKE2b-256 |
a3b92bd4c4a29b22c4e8fbfd0e23a975d3e319ee19cb63c10ebd01cbb5e66031
|
Provenance
The following attestation bundles were made for hctef-0.3.1-py3-none-any.whl:
Publisher:
release.yml on jkeifer/hctef
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
hctef-0.3.1-py3-none-any.whl -
Subject digest:
08ded822bc850b7be5f3e3fa8ed380818c9a3f4ad3c70ee9f93b9057c01290ac - Sigstore transparency entry: 2083874790
- Sigstore integration time:
-
Permalink:
jkeifer/hctef@bcee35a6ec937c621eed78a61eea47ea334fdf79 -
Branch / Tag:
refs/tags/v0.3.1 - Owner: https://github.com/jkeifer
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@bcee35a6ec937c621eed78a61eea47ea334fdf79 -
Trigger Event:
release
-
Statement type: