A clean, type-safe Python client for multi-source media APIs (Wallhaven, Unsplash, Reddit)
Project description
xanax
A clean, type-safe Python client for multi-source media APIs — Wallhaven, Unsplash, and Reddit.
Features
- Typed all the way down — every API response is a Pydantic model with full type hints
- Validated before sending — invalid parameters raise errors before any network request
- Both sync and async — sync clients for scripts; async clients for web apps and pipelines
- Multi-source — Wallhaven, Unsplash, and Reddit share the same
download()anditer_media()contract - Auto-pagination —
iter_media()andaiter_media()walk through all pages automatically - Secure by default — API keys go in headers, never in query strings
- Rate limit aware — configurable retry with exponential backoff on 429 responses
Supported sources
| Source | Sync | Async | Auth |
|---|---|---|---|
| Wallhaven | Wallhaven |
AsyncWallhaven |
WALLHAVEN_API_KEY (optional for SFW) |
| Unsplash | Unsplash |
AsyncUnsplash |
UNSPLASH_ACCESS_KEY (required) |
Reddit |
AsyncReddit |
REDDIT_CLIENT_ID + REDDIT_CLIENT_SECRET |
Installation
pip install xanax
uv add xanax
Quick start
from xanax import Wallhaven
from xanax.sources.wallhaven.params import SearchParams
client = Wallhaven(api_key="your-api-key")
for wallpaper in client.iter_media(SearchParams(query="nature")):
client.download(wallpaper, path=f"{wallpaper.id}.jpg")
from xanax import Unsplash
from xanax.sources.unsplash.params import UnsplashSearchParams
client = Unsplash(access_key="your-access-key")
for photo in client.iter_media(UnsplashSearchParams(query="mountains")):
client.download(photo, path=f"{photo.id}.jpg")
from xanax import Reddit
from xanax.sources.reddit.params import RedditParams
from xanax.sources.reddit.enums import RedditSort
client = Reddit(
client_id="your-client-id",
client_secret="your-client-secret",
user_agent="python:myapp/1.0 (by u/yourname)",
)
for post in client.iter_media(RedditParams(subreddit="EarthPorn", sort=RedditSort.TOP)):
client.download(post, path=f"{post.id}.jpg")
Authentication
Credentials can be passed directly or read from environment variables:
import os
# Wallhaven — optional for SFW content
os.environ["WALLHAVEN_API_KEY"] = "..."
client = Wallhaven()
# Unsplash — required
os.environ["UNSPLASH_ACCESS_KEY"] = "..."
client = Unsplash()
# Reddit — client_id, client_secret, and user_agent all required
os.environ["REDDIT_CLIENT_ID"] = "..."
os.environ["REDDIT_CLIENT_SECRET"] = "..."
os.environ["REDDIT_USER_AGENT"] = "python:myapp/1.0 (by u/yourname)"
client = Reddit()
Async support
Every sync client has an async counterpart with identical methods:
import asyncio
from xanax import AsyncWallhaven, AsyncUnsplash, AsyncReddit
from xanax.sources.wallhaven.params import SearchParams
async def main():
async with AsyncWallhaven(api_key="your-api-key") as client:
async for wallpaper in client.aiter_media(SearchParams(query="space")):
await client.download(wallpaper, path=f"{wallpaper.id}.jpg")
asyncio.run(main())
Source-agnostic code
All clients satisfy MediaSource / AsyncMediaSource, so you can write code that works
with any source:
from xanax.sources._base import MediaSource
def download_all(source: MediaSource, params) -> None:
for media in source.iter_media(params):
source.download(media, path=f"{media.id}.jpg")
download_all(Wallhaven(), SearchParams(query="anime"))
download_all(Unsplash(), UnsplashSearchParams(query="nature"))
Downloading
download() returns raw bytes and optionally saves to disk:
# Return bytes
data: bytes = client.download(media)
# Save to disk
client.download(media, path="output.jpg")
Error handling
from xanax.errors import (
XanaxError, # Base exception
AuthenticationError, # 401 or missing credentials
RateLimitError, # 429 — has .retry_after attribute
NotFoundError, # 404
ValidationError, # Invalid parameters (before any request)
APIError, # Other HTTP errors — has .status_code
)
try:
results = client.search(params)
except AuthenticationError:
print("Invalid or missing credentials")
except RateLimitError as e:
print(f"Rate limited. Retry after {e.retry_after}s")
except ValidationError as e:
print(f"Bad parameters: {e}")
Enable automatic retry on rate limits:
client = Wallhaven(max_retries=3) # exponential backoff on 429
Development
uv sync --extra dev
uv run pytest # run tests
uv run pytest --cov=xanax # with coverage
uv run mypy xanax/ # type check
uv run ruff check xanax/ tests/ # lint
Documentation
Full API reference and guides: xanax.readthedocs.io
License
BSD 3-Clause
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 xanax-0.3.2.tar.gz.
File metadata
- Download URL: xanax-0.3.2.tar.gz
- Upload date:
- Size: 128.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
80ebf24361f73c7d072b8389a05b9ac713a503849d5ea7784b38f5451b99c041
|
|
| MD5 |
0bd7b991f734e26fc3ab8e4ae9af62ca
|
|
| BLAKE2b-256 |
35f84b46b2ccd8d6da2530ebe2c1ebdd37e21440c1f18f2c87f6dfda505334a0
|
Provenance
The following attestation bundles were made for xanax-0.3.2.tar.gz:
Publisher:
publish.yml on violhex/Xanax
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
xanax-0.3.2.tar.gz -
Subject digest:
80ebf24361f73c7d072b8389a05b9ac713a503849d5ea7784b38f5451b99c041 - Sigstore transparency entry: 1010743524
- Sigstore integration time:
-
Permalink:
violhex/Xanax@ad270917ee6e80ba451b37faa8f1cff2c3055c72 -
Branch / Tag:
refs/tags/v0.3.2 - Owner: https://github.com/violhex
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@ad270917ee6e80ba451b37faa8f1cff2c3055c72 -
Trigger Event:
push
-
Statement type:
File details
Details for the file xanax-0.3.2-py3-none-any.whl.
File metadata
- Download URL: xanax-0.3.2-py3-none-any.whl
- Upload date:
- Size: 54.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
512389d0117441afa18fa44f6f9e8b5992281dfdf67287f524d2a84f4becfe1f
|
|
| MD5 |
702cb2409df059245949d3225d72447e
|
|
| BLAKE2b-256 |
abe163edacdb9d49729c96ca5ddf4184a0cc422a8d0ed42722198af3241bef51
|
Provenance
The following attestation bundles were made for xanax-0.3.2-py3-none-any.whl:
Publisher:
publish.yml on violhex/Xanax
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
xanax-0.3.2-py3-none-any.whl -
Subject digest:
512389d0117441afa18fa44f6f9e8b5992281dfdf67287f524d2a84f4becfe1f - Sigstore transparency entry: 1010743669
- Sigstore integration time:
-
Permalink:
violhex/Xanax@ad270917ee6e80ba451b37faa8f1cff2c3055c72 -
Branch / Tag:
refs/tags/v0.3.2 - Owner: https://github.com/violhex
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@ad270917ee6e80ba451b37faa8f1cff2c3055c72 -
Trigger Event:
push
-
Statement type: