Metadata-Version: 2.5
Name: valotcg
Version: 1.0.1
Summary: Official Python client for Valo.Data, the ValoTCG trading card price API.
Project-URL: Homepage, https://valotcg.com/developers
Project-URL: Documentation, https://valotcg.com/developers/docs
Project-URL: Repository, https://github.com/valotcg/valo-data-python
Project-URL: Changelog, https://github.com/valotcg/valo-data-python/blob/main/CHANGELOG.md
Project-URL: OpenAPI, https://api.valotcg.com/data/v1/openapi.json
Author: ValoTCG
License-Expression: MIT
License-File: LICENSE
Keywords: api client,card prices,tcg,trading cards,valo-data,valotcg
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: httpx<1,>=0.27
Requires-Dist: pydantic<3,>=2.6
Description-Content-Type: text/markdown

# valotcg: Python client for Valo.Data

Official Python client for [Valo.Data](https://valotcg.com/developers), the ValoTCG trading card price API:
catalogue, prices, price history and analysis for 14 TCGs (Pokémon, One Piece, Yu-Gi-Oh!, Magic, Lorcana and more),
with Cardmarket, US market and eBay sources.

- Sync (`ValoData`) and async (`AsyncValoData`) clients on [httpx](https://www.python-httpx.org/)
- Typed pydantic v2 models generated from the [OpenAPI document](https://api.valotcg.com/data/v1/openapi.json)
- Credits and request id on every response, typed errors, automatic retry on transient rate limits
- Cursor pagination helpers

Python 3.10 or later.

## Install

```bash
pip install valotcg
# or
uv add valotcg
```

## API key

Create a free Sandbox key at [valotcg.com/developers/account](https://valotcg.com/developers/account):
500 credits and 250 distinct cards per month, 100 billed requests per day, no credit card.
Plans and credit costs: [valotcg.com/developers/pricing](https://valotcg.com/developers/pricing).

The client reads the key from `api_key=` or from the `VALO_DATA_API_KEY` environment variable and sends it as
`Authorization: Bearer <key>`.

```bash
export VALO_DATA_API_KEY=vd_test_...
```

## Identify a card, then get its price summary

```python
from valotcg import ValoData

with ValoData() as client:
    found = client.analysis.identify("charizard 4/102", game="pokemon")
    card = found.data.candidates[0].card
    summary = client.analysis.card_summary(card.card_id)
    print(card.name, summary.data.recommended)
    print("credits used:", summary.meta.credits_used, "remaining:", summary.meta.credits_remaining)
```

## Batch prices

Up to 100 ids per call; sealed products are written `"sealed:<id>"`.

```python
batch = client.cards.batch([1234, 5678, "sealed:42"])
for item in batch.data.items:
    if item.status == "ok":
        for quote in item.prices or []:
            print(item.id, quote.source, quote.metric, quote.amount, quote.currency)
```

## Pagination

Search endpoints return one page and a `next_cursor`. The iterators fetch the next pages lazily
(each page is a billed request, stop iterating to stop fetching):

```python
for card in client.cards.iter_search("pokemon", q="pikachu", language="fr"):
    print(card.card_id, card.name)

# Any paginated method, sync or async:
from valotcg import apaginate, paginate

for edition in paginate(client.editions.list, "onepiece"):
    print(edition.name)
```

Available iterators: `cards.iter_search`, `sealed.iter_search`, `editions.iter_list`.

## Async

```python
import asyncio
from valotcg import AsyncValoData


async def main() -> None:
    async with AsyncValoData() as client:
        movers = await client.analysis.top_movers("pokemon", period=7, direction="up")
        for mover in movers.data.items:
            print(mover)
        async for card in client.cards.iter_search("lorcana", q="elsa"):
            print(card.name)


asyncio.run(main())
```

## Responses

Every method returns a `Response[T]`:

- `response.data`: the typed model (`valotcg.models`)
- `response.meta`: `credits_used`, `credits_remaining`, `request_id` (headers `X-Credits-Used`,
  `X-Credits-Remaining`, `X-Request-Id`), plus `rate_limit_limit`, `rate_limit_remaining`,
  `rate_limit_reset`, `status_code` and raw `headers`
- shortcuts: `response.credits_used`, `response.credits_remaining`, `response.request_id`, and attribute
  access falls through to `.data` (`response.candidates` is `response.data.candidates`)

Free routes (`games.list`, `games.rarities`, `usage`) may return no credit headers; the fields are then `None`.

## Routes

| Method | HTTP route |
|---|---|
| `client.games.list()` | `GET /data/v1/games` |
| `client.games.rarities(game)` | `GET /data/v1/games/{game}/rarities` |
| `client.editions.list(game, q=, language=, cursor=, page_size=)` | `GET /data/v1/editions` |
| `client.cards.search(game, q=, edition_id=, language=, rarity=, cursor=, page_size=)` | `GET /data/v1/cards` |
| `client.cards.get(card_id)` | `GET /data/v1/cards/{card_id}` |
| `client.cards.prices(card_id, include=)` | `GET /data/v1/cards/{card_id}/prices` |
| `client.cards.batch(ids, include=)` | `POST /data/v1/cards/batch` |
| `client.cards.price_history(card_id, source=, variant=, from_=, to=, market_language=, condition=)` | `GET /data/v1/cards/{card_id}/price-history` |
| `client.sealed.search(game, q=, edition_id=, product_type=, language=, cursor=, page_size=)` | `GET /data/v1/sealed` |
| `client.sealed.get(sealed_id)` | `GET /data/v1/sealed/{sealed_id}` |
| `client.sealed.prices(sealed_id)` | `GET /data/v1/sealed/{sealed_id}/prices` |
| `client.sealed.price_history(sealed_id, source=, from_=, to=)` | `GET /data/v1/sealed/{sealed_id}/price-history` |
| `client.analysis.identify(q, game=, language=)` | `GET /data/v1/analysis/identify` |
| `client.analysis.card_summary(card_id, include=)` | `GET /data/v1/analysis/cards/{card_id}/summary` |
| `client.analysis.sealed_summary(sealed_id)` | `GET /data/v1/analysis/sealed/{sealed_id}/summary` |
| `client.analysis.compare(ids, include=)` | `POST /data/v1/analysis/compare` |
| `client.analysis.top_movers(game, period=, direction=, edition_id=, language=, limit=, reliable=)` | `GET /data/v1/analysis/top-movers` |
| `client.analysis.top_priced(game, kind=, language=, edition_id=, product_type=, limit=)` | `GET /data/v1/analysis/top-priced` |
| `client.analysis.edition_overview(edition_id)` | `GET /data/v1/analysis/editions/{edition_id}/overview` |
| `client.usage(product=)` | `GET /developer/v1/usage` |

`from_` maps to the `from` query parameter. `include` accepts a list (`["graded", "population"]`) or a
comma separated string. Price sources are `cardmarket`, `us_market` and `ebay`.

## Errors

Error bodies `{"error": {"code", "message", "request_id", "docs_url"}}` raise typed exceptions, all subclasses
of `valotcg.ValoDataError` with `status_code`, `code`, `message`, `request_id` and `docs_url`:

| Exception | Status | Example codes |
|---|---|---|
| `AuthenticationError` | 401 | `invalid_api_key`, `api_key_expired` |
| `PermissionDeniedError` | 403 | `insufficient_scope`, `plan_feature_unavailable`, `ip_not_allowed` |
| `NotFoundError` | 404 | `card_not_found`, `sealed_not_found`, `edition_not_found` |
| `ValidationError` | 400, 422 | `invalid_request`, `invalid_cursor`, `range_exceeds_plan` |
| `RateLimitError` | 429 | `rate_limited`, `quota_exceeded`, `daily_limit_reached`, `catalog_limit_reached` |
| `ServerError` | 5xx | `quota_unavailable` |
| `APIConnectionError` | none | network failure or timeout |

`RateLimitError` also carries `retry_after` (seconds, from `Retry-After`) and `resets_at` (when a credit limit
resets). `valotcg.ValidationError` is distinct from `pydantic.ValidationError`.

```python
from valotcg import NotFoundError, RateLimitError

try:
    client.cards.get(999999999)
except NotFoundError as exc:
    print(exc.code, exc.request_id)
except RateLimitError as exc:
    print(exc.code, exc.resets_at)
```

### Retries

A `429` with code `rate_limited` (or `concurrency_limited`) is retried after `Retry-After` seconds
(capped at 60), up to `max_retries` times (default 2). Credit limits (`quota_exceeded`,
`daily_limit_reached`, `catalog_limit_reached`, `key_credit_cap_reached`) are never retried.
Disable retries with `ValoData(max_retries=0)`.

## Configuration

```python
ValoData(
    api_key=None,  # default: VALO_DATA_API_KEY
    base_url=None,  # default: VALO_DATA_BASE_URL or https://api.valotcg.com
    timeout=30.0,  # seconds, or an httpx.Timeout
    max_retries=2,
    http_client=None,  # your own httpx.Client (AsyncValoData: httpx.AsyncClient)
)
```

## Links

- Documentation: [valotcg.com/developers/docs](https://valotcg.com/developers/docs)
- OpenAPI: [api.valotcg.com/data/v1/openapi.json](https://api.valotcg.com/data/v1/openapi.json)
  (snapshot pinned in [`openapi.json`](openapi.json))
- Postman: [postman.com/valotcg/valotcg](https://www.postman.com/valotcg/valotcg)
- MCP server: [github.com/valotcg/valo-ia-mcp](https://github.com/valotcg/valo-ia-mcp)
- RapidAPI: [rapidapi.com/Stephd91/api/valo-data-trading-card-prices](https://rapidapi.com/Stephd91/api/valo-data-trading-card-prices)
- Plans: [valotcg.com/developers/pricing](https://valotcg.com/developers/pricing)

## Attribution

The terms of use require an attribution wherever the data is shown:
"Data: [ValoTCG](https://valotcg.com)".

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md), including how to regenerate the models from the OpenAPI document.

## License

MIT, see [LICENSE](LICENSE).
