A clean, type-safe Python client for the Wallhaven.cc API v1
Project description
xanax
A clean, type-safe Python client for the Wallhaven.cc API v1.
Features
- Type-safe: All responses are parsed into Pydantic models with full type hints
- Validated parameters: Search parameters are validated before making API requests
- Clean error handling: Structured error hierarchy for easy error handling
- Rate limit aware: Handles rate limiting with optional retry support
- Secure: API key is never logged or exposed in any output
- Async support: Full async client (
AsyncXanax) built onhttpx.AsyncClient - Auto-pagination:
iter_pages()/aiter_pages()handle all pages automatically - Download helper:
client.download(wallpaper)fetches raw bytes and optionally saves to disk - Env var auth: API key can be set via
WALLHAVEN_API_KEYenvironment variable
Installation
pip install xanax
Or with uv:
uv add xanax
Quick Start
from xanax import Xanax, SearchParams, Purity, Sort
# API key can also be set via WALLHAVEN_API_KEY env var
client = Xanax(api_key="your-api-key")
# Search for wallpapers (all categories included by default)
params = SearchParams(
query="+anime -sketch",
purity=[Purity.SFW],
sorting=Sort.TOPLIST,
)
results = client.search(params)
# Iterate through results
for wallpaper in results.data:
print(f"{wallpaper.resolution} - {wallpaper.path}")
# Check pagination
print(f"Page {results.meta.current_page} of {results.meta.last_page}")
Authentication
The Wallhaven API requires an API key for NSFW content. You can get your API key from your Wallhaven account settings.
# Explicit key
client = Xanax(api_key="your-api-key")
# Or set WALLHAVEN_API_KEY in your environment
import os
os.environ["WALLHAVEN_API_KEY"] = "your-api-key"
client = Xanax() # picks it up automatically
The API key is stored securely and never exposed in any string representations.
Async Client
AsyncXanax mirrors the sync client exactly, using httpx.AsyncClient internally:
import asyncio
from xanax import AsyncXanax, SearchParams
async def main():
async with AsyncXanax(api_key="your-api-key") as client:
results = await client.search(SearchParams(query="anime"))
# Auto-paginate asynchronously
async for wallpaper in client.aiter_wallpapers(SearchParams(query="nature")):
print(wallpaper.path)
asyncio.run(main())
Auto-Pagination
Both clients expose generators that handle pagination automatically, including seed propagation for random-sorted results:
# Sync: iterate over every wallpaper across all pages
for wallpaper in client.iter_wallpapers(SearchParams(query="space")):
print(wallpaper.id)
# Sync: iterate page by page
for page in client.iter_pages(SearchParams(query="space")):
print(f"Page {page.meta.current_page}: {len(page.data)} results")
# Async equivalents
async for wallpaper in client.aiter_wallpapers(SearchParams(query="space")):
print(wallpaper.id)
Downloading Wallpapers
wallpaper = client.wallpaper("94x38z")
# Download to memory
data: bytes = client.download(wallpaper)
# Download and save to disk
client.download(wallpaper, path="wallpaper.jpg")
# Async
data = await async_client.download(wallpaper, path="wallpaper.jpg")
Search Parameters
SearchParams provides type-safe search parameters with pre-flight validation:
from xanax import SearchParams
from xanax.enums import Category, Purity, Sort, Order, TopRange, Color, FileType
params = SearchParams(
query="+nature -water", # Include/exclude tags
categories=[Category.GENERAL, Category.ANIME],
purity=[Purity.SFW], # SFW, SKETCHY, NSFW (requires API key)
sorting=Sort.TOPLIST, # date_added, relevance, random, views, favorites, toplist
order=Order.DESC, # desc or asc
top_range=TopRange.ONE_MONTH, # 1d, 3d, 1w, 1M, 3M, 6M, 1y (toplist only)
resolutions=["1920x1080", "2560x1440"],
ratios=["16x9", "4x3"],
colors=[Color.BLUE, Color.GREEN],
file_type=FileType.PNG, # Filter by file type
like="94x38z", # Find wallpapers similar to this ID
page=1,
seed="abc123", # For random sorting consistency
)
with_page() and with_seed() return new instances with the field updated:
page2_params = params.with_page(2)
seeded_params = params.with_seed("xyz789")
API Reference
Xanax / AsyncXanax
client = Xanax(api_key=None, timeout=30.0, max_retries=0)
client = AsyncXanax(api_key=None, timeout=30.0, max_retries=0)
Methods (async variants are identical with await / async for):
| Method | Returns | Description |
|---|---|---|
wallpaper(id) |
Wallpaper |
Fetch a specific wallpaper |
search(params) |
SearchResult |
Search wallpapers |
tag(id) |
Tag |
Fetch tag info |
settings() |
UserSettings |
Authenticated user settings |
collections(username?) |
list[Collection] |
User collections |
collection(username, id) |
CollectionListing |
Wallpapers in a collection |
download(wallpaper, path?) |
bytes |
Download wallpaper image |
iter_pages(params) |
Iterator[SearchResult] |
Auto-paginate (sync) |
iter_wallpapers(params) |
Iterator[Wallpaper] |
Flat wallpaper iterator (sync) |
aiter_pages(params) |
AsyncIterator[SearchResult] |
Auto-paginate (async) |
aiter_wallpapers(params) |
AsyncIterator[Wallpaper] |
Flat wallpaper iterator (async) |
Error Handling
from xanax.errors import (
XanaxError, # Base exception
AuthenticationError, # 401 or missing API key
RateLimitError, # 429, has .retry_after attribute
NotFoundError, # 404
ValidationError, # Invalid parameters (raised before any request)
APIError, # Other HTTP errors, has .status_code attribute
)
try:
results = client.search(SearchParams(query="anime"))
except AuthenticationError:
print("Invalid or missing API key")
except RateLimitError as e:
print(f"Rate limited. Retry after {e.retry_after} seconds")
except NotFoundError:
print("Resource not found")
except ValidationError as e:
print(f"Invalid parameters: {e.message}")
except APIError as e:
print(f"API error: {e.status_code}")
Pagination Helper
For manual pagination control:
from xanax import PaginationHelper
results = client.search(params)
helper = PaginationHelper(results.meta)
print(helper.current_page, helper.last_page, helper.total)
if helper.has_next:
next_results = client.search(params.with_page(helper.next_page_number()))
Models
All API responses are parsed into typed Pydantic models:
Wallpaper- Single wallpaper with all metadataTag- Tag informationUploader- User uploader infoAvatar- User avatar at different sizesThumbnails- Thumbnail URLsSearchResult- Search results with wallpapers and paginationPaginationMeta- Pagination information (current page, last page, total, seed)QueryInfo- Resolved search query infoUserSettings- User account preferencesCollection- Collection metadataCollectionListing- Collection wallpapers with pagination
Enumerations
All search parameters have type-safe StrEnum members:
Category-GENERAL,ANIME,PEOPLEPurity-SFW,SKETCHY,NSFWSort-DATE_ADDED,RELEVANCE,RANDOM,VIEWS,FAVORITES,TOPLISTOrder-DESC,ASCTopRange-ONE_DAY,THREE_DAYS,ONE_WEEK,ONE_MONTH,THREE_MONTHS,SIX_MONTHS,ONE_YEARColor- 29 named color options (e.g.,Color.BLUE,Color.CRIMSON)FileType-PNG,JPG
Rate Limiting
The Wallhaven API allows 45 requests per minute. By default, the client raises RateLimitError
immediately on a 429 response. Enable automatic retry with exponential backoff:
client = Xanax(max_retries=3) # Retry up to 3 times with exponential backoff
Development
# Install development dependencies
uv sync --extra dev
# Run tests
uv run pytest
# Run with coverage
uv run pytest --cov=xanax
# Type check
uv run mypy xanax/
# Lint
uv run ruff check xanax/
License
MIT
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.2.0.tar.gz.
File metadata
- Download URL: xanax-0.2.0.tar.gz
- Upload date:
- Size: 86.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bd98e94ba4a1191cbe5d0655c2e15fe83f50fa01d89481c6617984e75e09218a
|
|
| MD5 |
87b530581727f560fb8715cb5721a72a
|
|
| BLAKE2b-256 |
abe7814309490fae172af399da67840eed8272ee5864fa6df79604deaf6d994b
|
Provenance
The following attestation bundles were made for xanax-0.2.0.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.2.0.tar.gz -
Subject digest:
bd98e94ba4a1191cbe5d0655c2e15fe83f50fa01d89481c6617984e75e09218a - Sigstore transparency entry: 1006079039
- Sigstore integration time:
-
Permalink:
violhex/Xanax@116f20a3dc45dc085713e7e8505f2d21accb9dc8 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/violhex
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@116f20a3dc45dc085713e7e8505f2d21accb9dc8 -
Trigger Event:
release
-
Statement type:
File details
Details for the file xanax-0.2.0-py3-none-any.whl.
File metadata
- Download URL: xanax-0.2.0-py3-none-any.whl
- Upload date:
- Size: 22.6 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 |
b02334a12885802b196e59bfb3ac3b2f48004d17ec95692342de3c616c1570dd
|
|
| MD5 |
e7829e177780183e6bba24b99e61289c
|
|
| BLAKE2b-256 |
b0f72e9eaf120e96c8364a754a47e0d1e15c4783fb9c45cccb75186cdb6e4ede
|
Provenance
The following attestation bundles were made for xanax-0.2.0-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.2.0-py3-none-any.whl -
Subject digest:
b02334a12885802b196e59bfb3ac3b2f48004d17ec95692342de3c616c1570dd - Sigstore transparency entry: 1006079091
- Sigstore integration time:
-
Permalink:
violhex/Xanax@116f20a3dc45dc085713e7e8505f2d21accb9dc8 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/violhex
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@116f20a3dc45dc085713e7e8505f2d21accb9dc8 -
Trigger Event:
release
-
Statement type: