Metadata-Version: 2.5
Name: carddex
Version: 0.2.0
Summary: Official Python client for the CardDex Pokémon TCG API
Project-URL: Homepage, https://carddex.dev/docs
Project-URL: Documentation, https://carddex.dev/docs
Author: CardDex
License-Expression: MIT
License-File: LICENSE
Keywords: api,carddex,pokemon,sdk,tcg,trading-cards
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
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: Typing :: Typed
Requires-Python: >=3.9
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == 'dev'
Description-Content-Type: text/markdown

# carddex

Official Python client for the [CardDex](https://carddex.dev) Pokémon TCG API — cards, sets, print variants, sealed products, and price history from Cardmarket (EUR) and TCGplayer (USD).

- Zero runtime dependencies — standard library only (`urllib`). See "Why no httpx?" below.
- Python 3.9+, fully typed (`TypedDict`s, `py.typed` marker).
- Sync `CardDex` client, plus an `AsyncCardDex` for `async`/`await` code.

## Install

```bash
pip install carddex
```

## Quickstart

```python
from carddex import CardDex

client = CardDex(api_key="pk_live_...")  # api_key is optional — see "Anonymous access" below

cards = client.cards.list(name="Charizard", sort="release_date", order="desc")["data"]

card = client.cards.get("sv06-001", include=["prices", "images"])

prices = client.cards.prices("sv06-001")

history = client.cards.price_history("sv06-001", days=180, source=["cardmarket", "tcgplayer"])
```

### Get a free API key

```python
from carddex import CardDex

client = CardDex()
result = client.auth.register("you@example.com", "My App")
api_key = result["api_key"]
# Save api_key now — it is shown only once. If you lose it, sign in at
# https://carddex.dev/account to create a new one or rotate/revoke keys.
```

### Async

```python
import asyncio
from carddex import AsyncCardDex

async def main():
    client = AsyncCardDex(api_key="pk_live_...")
    card = await client.cards.get("sv06-001")
    async for card in client.cards.list_all(set="sv06"):
        print(card["name"])

asyncio.run(main())
```

## Authentication and anonymous access

The client sends your key in the `X-API-Key` header. You can sign in with an email link at [carddex.dev/account](https://carddex.dev/account) to see your keys, create up to 2 active keys, and rotate or revoke them.

An `api_key` is optional. CardDex allows anonymous access at a lower but real rate limit (30 req/min, 5,000 req/day per IP). If you're distributing this client inside an app end users run themselves (a desktop tool, a CLI, a mobile app), the same rule from the web docs applies: don't embed a real API key in code you ship to other people — it becomes everyone's key. Anonymous access, or a backend you control, is the right choice there.

## Pagination

Every list endpoint returns `{"data": [...], "meta": {"total", "page", "pageSize", "totalPages"}}`. Each resource that lists something also exposes a `*_all` method that returns an iterator (or, on `AsyncCardDex`, an async iterator), fetching pages lazily as you consume them:

```python
for card in client.cards.list_all(set="sv06"):
    print(card["name"])

for product in client.sealed.list_all(set_id="sv06"):
    ...

for card in client.sets.cards_all("sv06"):
    ...
```

You can also use the lower-level `paginate()` helper directly against any page-shaped fetcher:

```python
from carddex import paginate

for card in paginate(lambda page: client.cards.list(set="sv06", page=page)):
    ...
```

## Errors

Every non-2xx response raises `CardDexError`:

```python
from carddex import CardDex, CardDexError

client = CardDex()

try:
    client.cards.get("does-not-exist")
except CardDexError as err:
    err.status          # HTTP status, e.g. 404
    err.code             # the API's error.code (an int for a real API response; 'NETWORK_ERROR' for a transport failure)
    str(err)             # the API's error.message
    err.retry_after      # seconds to wait, parsed from Retry-After on a 429 — None otherwise
    err.rate_limit       # RateLimitInfo(limit, daily_limit, daily_remaining) parsed from response headers, or None
    err.is_rate_limited  # True for a 429 (either the per-minute or the daily cap)
```

## Rate limits & retries

Every response carries `X-RateLimit-Limit` (per minute), `X-RateLimit-Daily-Limit` and `X-RateLimit-Daily-Remaining` (per UTC day, approximate). A 429 carries `Retry-After` in seconds: `60` for the per-minute limit, or the seconds until 00:00 UTC for the daily one.

Automatic retry on 429 is **off by default**. Turn it on when you want it:

```python
from carddex import CardDex, RetryOptions

client = CardDex(retry=True)  # up to 2 retries, never waiting more than 60s
client2 = CardDex(retry=RetryOptions(max_retries=3, max_wait_seconds=30))
```

A `Retry-After` longer than `max_wait_seconds` is never honoured — the call fails immediately instead of sleeping. This is deliberate: the daily-quota 429's `Retry-After` can be most of a day, and a small SDK default should never block a thread for hours. The per-minute 429's `Retry-After: 60` fits comfortably under the default 60s cap, so that case retries; a daily-cap 429 does not, by design.

## Why no httpx?

This package has **zero runtime dependencies** — it uses `urllib.request` from the standard library rather than `httpx` or `requests`. The tradeoffs, and why we picked this side of them:

- It matches the sibling [`@carddex/sdk`](https://www.npmjs.com/package/@carddex/sdk) TypeScript package's own zero-dependency design goal — a thin API client shouldn't hand you a dependency tree.
- No version pinning or conflicts with whatever HTTP stack your own project already uses. A thin client that adds `httpx>=0.27` can quietly force a resolver fight in an app that pins a different major version, or already standardized on `requests`.
- `urllib.request` handles everything this client actually needs: JSON in, JSON out, status codes, headers. This API has no streaming responses, no HTTP/2 requirement, no cookie jars.

The real cost is `AsyncCardDex`: there's no async HTTP client in the standard library, so it wraps the sync client with `asyncio.to_thread` instead of speaking a native async transport — see the docstring in `carddex/aio.py`. Every call is genuinely awaitable and concurrent calls via `asyncio.gather` work correctly, but a single call doesn't get the latency/throughput benefit of a real async socket. If your workload is dominated by highly concurrent CardDex calls and this matters for you, point `httpx.AsyncClient` at the same endpoints yourself, or email api@carddex.dev — a `carddex[httpx]` extra transport is a reasonable future addition if there's demand.

## Migrating from pokemontcg.io

The pokemontcg.io v2 compatibility layer is live: an app built on pokemontcg.io v2 can keep its code and stored ids and change its base URL to `https://api.carddex.dev/compat/pokemontcg/v2`. The guide at [carddex.dev/migrate/pokemontcg](https://carddex.dev/migrate/pokemontcg) covers what the layer supports, which official pokemontcg.io SDKs work with it, and the fields that differ. (The official pokemontcg.io Python SDK does not work with the layer yet; the guide explains why and what to use instead.)

This SDK talks to the native `/v1` API, which uses CardDex ids (`sv02-062`) but accepts pokemontcg.io ids as well: `client.cards.get("sv2-62")` returns the same card. The API also returns a `ptcgio_id` field on every card and accepts a `ptcgio_id` filter on the card list; this SDK's types don't include either yet.

## API surface

| Resource | Methods |
| --- | --- |
| `client.cards` | `list`, `list_all`, `get`, `random`, `prices`, `price_history` |
| `client.sets` | `list`, `get`, `cards`, `cards_all`, `neighbors`, `sealed` |
| `client.sealed` | `list`, `list_all`, `get`, `prices` |
| `client.prices` | `bulk`, `top`, `trends`, `bargains`, `history` |
| `client.auth` | `register`, `usage` |
| `client` | `usage()` (shorthand for `client.auth.usage()`), `health()` |

`AsyncCardDex` mirrors the same resources and methods, all awaitable (`*_all` methods become async iterators).

## Development (this monorepo)

Requires Python 3.9+.

```bash
py -m pip install -e ".[dev]"   # editable install with pytest
py -m pytest
```

**Known spec gap:** the OpenAPI spec currently documents paths, parameters and status codes but not response body schemas. The `TypedDict`s in `carddex/types.py` are hand-written from the API's own internal row shapes and should be regenerated once the API's OpenAPI spec grows `content.schema` on its responses.

## License

MIT
