Metadata-Version: 2.4
Name: pyhaul
Version: 0.4.0
Summary: Resumable, cursor-based, CDN-safe HTTP downloads for Python
Keywords: http,download,resume,range,partial,aiohttp
Author: pyhaul contributors
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: System :: Networking
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Dist: aiohttp>=3.10 ; extra == 'aiohttp'
Requires-Dist: httpx>=0.27 ; extra == 'httpx'
Requires-Dist: niquests>=3.14 ; extra == 'niquests'
Requires-Dist: requests>=2.32 ; extra == 'requests'
Requires-Dist: urllib3>=2.0 ; extra == 'urllib3'
Requires-Python: >=3.12
Project-URL: Homepage, https://github.com/chad-loder/pyhaul
Project-URL: Documentation, https://github.com/chad-loder/pyhaul#readme
Project-URL: Issues, https://github.com/chad-loder/pyhaul/issues
Project-URL: Source, https://github.com/chad-loder/pyhaul
Project-URL: Changelog, https://github.com/chad-loder/pyhaul/blob/main/CHANGELOG.md
Provides-Extra: aiohttp
Provides-Extra: httpx
Provides-Extra: niquests
Provides-Extra: requests
Provides-Extra: urllib3
Description-Content-Type: text/markdown

# pyhaul

[![CI](https://github.com/chad-loder/pyhaul/actions/workflows/ci.yml/badge.svg)](https://github.com/chad-loder/pyhaul/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/chad-loder/pyhaul/graph/badge.svg)](https://codecov.io/gh/chad-loder/pyhaul)
[![PyPI](https://img.shields.io/pypi/v/pyhaul.svg)](https://pypi.org/project/pyhaul/)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/chad-loder/pyhaul/blob/main/LICENSE)

Resumable HTTP downloads for Python. **Bring your own client:** pyhaul borrows your existing
session and handles byte-range negotiation, crash-safe checkpointing, and validation.

[![httpx](https://img.shields.io/badge/httpx-async%2Bsync-6B46C1.svg)](https://www.python-httpx.org/)
[![niquests](https://img.shields.io/badge/niquests-async%2Bsync-6B46C1.svg)](https://niquests.readthedocs.io/)
[![aiohttp](https://img.shields.io/badge/aiohttp-async-2563EB.svg)](https://docs.aiohttp.org/)
[![requests](https://img.shields.io/badge/requests-sync-059669.svg)](https://requests.readthedocs.io/)
[![urllib3](https://img.shields.io/badge/urllib3-sync-059669.svg)](https://urllib3.readthedocs.io/)

```bash
pip install pyhaul[httpx]   # or: niquests, requests, urllib3, aiohttp
```

```python
import httpx  # or: requests, niquests, urllib3, aiohttp
from pyhaul import haul

with httpx.Client() as client:
    result = haul("https://example.com/big.zip", client, dest="big.zip")
    print(f"done: sha256={result.sha256[:16]}…")
```

---

## What is it?

A small, pure-Python library that makes HTTP downloads **resumable**.
Call `haul()` with your existing HTTP client, a URL, and a destination
path — it handles byte-range negotiation, ETag validation, crash-safe
checkpointing, and atomic file completion. Sync and async; works with
requests, httpx, niquests, urllib3, and **aiohttp** (async).

Each call to `haul()` upholds these guarantees:

- **The destination file is either complete or absent.** There is no
  state where a partially-written file sits at the final path.
  Incomplete data lives in a temporary `.part` file; on completion
  it is atomically moved into place.
- **Interrupted downloads resume, not restart.** Checkpoint state
  lives on disk, not in memory. Kill the process, lose the network,
  get a 503 — the next `haul()` picks up from the last durable
  byte. Zero re-downloaded data if the resource hasn't changed.
- **Changed resources are detected, not silently corrupted.** If
  the remote file changes between attempts, `pyhaul` detects the
  mismatch via ETag (a server-side fingerprint) and starts over
  cleanly instead of gluing mismatched halves together.
- **Your HTTP client is borrowed, not owned.** `pyhaul` sets
  per-request headers and returns the session untouched. It never
  creates, configures, or closes sessions.
- **Transport errors pass through unwrapped.** `httpx.ReadTimeout`
  stays `httpx.ReadTimeout`. You catch the types you already know.

## How it fits into your code

One `haul()` = one HTTP request. It either succeeds and returns
`CompleteHaul`, or it throws — possibly after saving progress
to a `.part` file that allows the next call to resume. `pyhaul` never
creates sessions, connections, or clients. Your HTTP library's native
exceptions propagate through unwrapped, so you can drop `haul()`
into existing code without changing your error handling. Retries are
your call — a for-loop, `tenacity`, or nothing. Concurrency limiting
(e.g. `asyncio.Semaphore`) is also yours — `pyhaul` downloads one
file per call and doesn't manage parallelism.

```python
def haul(url, client, *, dest, state=None) -> CompleteHaul: ...
async def haul_async(url, client, *, dest, state=None) -> CompleteHaul: ...
```

`state` is an optional `HaulState` bag, updated in-place as bytes
land on disk — works identically in sync and async. See
[DESIGN.md](https://github.com/chad-loder/pyhaul/blob/main/DESIGN.md) for the exception hierarchy, transport
adapters, and download lifecycle.

## Documentation

- **[Design](https://github.com/chad-loder/pyhaul/blob/main/DESIGN.md)** — transport adapters, checkpoint state, download lifecycle
- **[Why this exists](https://github.com/chad-loder/pyhaul/blob/main/WHY.md)** — failure modes and comparison with `curl` / `wget` / `aria2c`
- **[Specification](https://github.com/chad-loder/pyhaul/blob/main/docs/SPEC.md)** — control file format
