Skip to main content

A clean, type-safe Python client for multi-source media APIs (Wallhaven, Unsplash, Reddit)

Project description

xanax

PyPI Stars Docs

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() and iter_media() contract
  • Auto-paginationiter_media() and aiter_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 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

xanax-0.3.2.tar.gz (128.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

xanax-0.3.2-py3-none-any.whl (54.2 kB view details)

Uploaded Python 3

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

Hashes for xanax-0.3.2.tar.gz
Algorithm Hash digest
SHA256 80ebf24361f73c7d072b8389a05b9ac713a503849d5ea7784b38f5451b99c041
MD5 0bd7b991f734e26fc3ab8e4ae9af62ca
BLAKE2b-256 35f84b46b2ccd8d6da2530ebe2c1ebdd37e21440c1f18f2c87f6dfda505334a0

See more details on using hashes here.

Provenance

The following attestation bundles were made for xanax-0.3.2.tar.gz:

Publisher: publish.yml on violhex/Xanax

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for xanax-0.3.2-py3-none-any.whl
Algorithm Hash digest
SHA256 512389d0117441afa18fa44f6f9e8b5992281dfdf67287f524d2a84f4becfe1f
MD5 702cb2409df059245949d3225d72447e
BLAKE2b-256 abe163edacdb9d49729c96ca5ddf4184a0cc422a8d0ed42722198af3241bef51

See more details on using hashes here.

Provenance

The following attestation bundles were made for xanax-0.3.2-py3-none-any.whl:

Publisher: publish.yml on violhex/Xanax

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page