Metadata-Version: 2.4
Name: xanax
Version: 0.3.2
Summary: A clean, type-safe Python client for multi-source media APIs (Wallhaven, Unsplash, Reddit)
Project-URL: Homepage, https://github.com/violhex/xanax
Project-URL: Documentation, https://xanax.readthedocs.io/en/stable/
Project-URL: Repository, https://github.com/violhex/xanax
Project-URL: Changelog, https://xanax.readthedocs.io/en/stable/changelog/
Project-URL: Bug Tracker, https://github.com/violhex/xanax/issues
Author: xanax
License: BSD-3-Clause
License-File: LICENSE
Keywords: api,async,client,image,media,photo,pydantic,reddit,typed,unsplash,wallhaven,wallpaper
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: BSD License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Multimedia
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.12
Requires-Dist: httpx<1.0.0,>=0.27.0
Requires-Dist: pydantic<3.0.0,>=2.0.0
Provides-Extra: dev
Requires-Dist: mypy>=1.8.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
Requires-Dist: pytest>=8.0.0; extra == 'dev'
Requires-Dist: ruff>=0.3.0; extra == 'dev'
Provides-Extra: docs
Requires-Dist: furo>=2024.1.29; extra == 'docs'
Requires-Dist: myst-parser>=3.0; extra == 'docs'
Requires-Dist: sphinx-autodoc-typehints>=2.0; extra == 'docs'
Requires-Dist: sphinx>=7.0; extra == 'docs'
Description-Content-Type: text/markdown

# xanax

[![PyPI](https://img.shields.io/pypi/v/xanax)](https://pypi.org/project/xanax/)
[![Stars](https://img.shields.io/github/stars/violhex/xanax?style=flat)](https://github.com/violhex/xanax/stargazers)
[![Docs](https://readthedocs.org/projects/xanax/badge/?version=stable)](https://xanax.readthedocs.io/en/stable/)

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-pagination** — `iter_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](https://wallhaven.cc) | `Wallhaven` | `AsyncWallhaven` | `WALLHAVEN_API_KEY` (optional for SFW) |
| [Unsplash](https://unsplash.com) | `Unsplash` | `AsyncUnsplash` | `UNSPLASH_ACCESS_KEY` (required) |
| [Reddit](https://reddit.com) | `Reddit` | `AsyncReddit` | `REDDIT_CLIENT_ID` + `REDDIT_CLIENT_SECRET` |

## Installation

```bash
pip install xanax
```

```bash
uv add xanax
```

## Quick start

```python
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")
```

```python
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")
```

```python
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:

```python
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:

```python
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:

```python
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:

```python
# Return bytes
data: bytes = client.download(media)

# Save to disk
client.download(media, path="output.jpg")
```

## Error handling

```python
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:

```python
client = Wallhaven(max_retries=3)  # exponential backoff on 429
```

## Development

```bash
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](https://xanax.readthedocs.io)

## License

BSD 3-Clause
