pokemontcgapi 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. pokemontcgapi-0.1.0/.gitignore +9 -0
  2. pokemontcgapi-0.1.0/LICENSE +21 -0
  3. pokemontcgapi-0.1.0/PKG-INFO +385 -0
  4. pokemontcgapi-0.1.0/README.md +350 -0
  5. pokemontcgapi-0.1.0/pyproject.toml +68 -0
  6. pokemontcgapi-0.1.0/src/pokemontcgapi/__init__.py +129 -0
  7. pokemontcgapi-0.1.0/src/pokemontcgapi/_http.py +378 -0
  8. pokemontcgapi-0.1.0/src/pokemontcgapi/_page.py +92 -0
  9. pokemontcgapi-0.1.0/src/pokemontcgapi/_version.py +3 -0
  10. pokemontcgapi-0.1.0/src/pokemontcgapi/client.py +191 -0
  11. pokemontcgapi-0.1.0/src/pokemontcgapi/errors.py +222 -0
  12. pokemontcgapi-0.1.0/src/pokemontcgapi/py.typed +0 -0
  13. pokemontcgapi-0.1.0/src/pokemontcgapi/resources.py +698 -0
  14. pokemontcgapi-0.1.0/src/pokemontcgapi/types.py +527 -0
  15. pokemontcgapi-0.1.0/tests/__init__.py +0 -0
  16. pokemontcgapi-0.1.0/tests/conftest.py +88 -0
  17. pokemontcgapi-0.1.0/tests/fixtures/304.json +7 -0
  18. pokemontcgapi-0.1.0/tests/fixtures/400-invalid-parameter.json +30 -0
  19. pokemontcgapi-0.1.0/tests/fixtures/401-invalid-key.json +15 -0
  20. pokemontcgapi-0.1.0/tests/fixtures/401-missing-key.json +16 -0
  21. pokemontcgapi-0.1.0/tests/fixtures/403-plan-required-next-step.json +26 -0
  22. pokemontcgapi-0.1.0/tests/fixtures/403-trial-expired.json +24 -0
  23. pokemontcgapi-0.1.0/tests/fixtures/404-card-not-found-did-you-mean.json +21 -0
  24. pokemontcgapi-0.1.0/tests/fixtures/429-quota-exceeded-next-step.json +29 -0
  25. pokemontcgapi-0.1.0/tests/fixtures/429-rate-limited.json +17 -0
  26. pokemontcgapi-0.1.0/tests/fixtures/500-html.json +7 -0
  27. pokemontcgapi-0.1.0/tests/fixtures/batch-missing.json +32 -0
  28. pokemontcgapi-0.1.0/tests/fixtures/card-bs-4-with-prices.json +132 -0
  29. pokemontcgapi-0.1.0/tests/fixtures/changes-page.json +28 -0
  30. pokemontcgapi-0.1.0/tests/fixtures/health.json +14 -0
  31. pokemontcgapi-0.1.0/tests/fixtures/make_fixtures.py +203 -0
  32. pokemontcgapi-0.1.0/tests/fixtures/prices-card-withheld.json +93 -0
  33. pokemontcgapi-0.1.0/tests/fixtures/prices-sources.json +56 -0
  34. pokemontcgapi-0.1.0/tests/fixtures/reference.json +205 -0
  35. pokemontcgapi-0.1.0/tests/fixtures/sets-page-1.json +55 -0
  36. pokemontcgapi-0.1.0/tests/fixtures/sets-page-2.json +55 -0
  37. pokemontcgapi-0.1.0/tests/fixtures/sets-page-3.json +36 -0
  38. pokemontcgapi-0.1.0/tests/fixtures/status.json +49 -0
  39. pokemontcgapi-0.1.0/tests/fixtures/vision-ambiguous.json +61 -0
  40. pokemontcgapi-0.1.0/tests/test_async.py +140 -0
  41. pokemontcgapi-0.1.0/tests/test_errors.py +181 -0
  42. pokemontcgapi-0.1.0/tests/test_resources.py +185 -0
  43. pokemontcgapi-0.1.0/tests/test_retry_and_cache.py +110 -0
  44. pokemontcgapi-0.1.0/tests/test_transport.py +113 -0
@@ -0,0 +1,9 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .mypy_cache/
9
+ .ruff_cache/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 pokemontcgapi.com
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,385 @@
1
+ Metadata-Version: 2.5
2
+ Name: pokemontcgapi
3
+ Version: 0.1.0
4
+ Summary: Python client for the pokemontcgapi.com Pokémon TCG API: cards, sets, artists, images and prices with stated provenance.
5
+ Project-URL: Homepage, https://pokemontcgapi.com/sdk
6
+ Project-URL: Documentation, https://pokemontcgapi.com/docs
7
+ Project-URL: Repository, https://github.com/pokemontcgapi/sdk-python
8
+ Project-URL: Issues, https://github.com/pokemontcgapi/sdk-python/issues
9
+ Author: pokemontcgapi.com
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: api,card-prices,japanese-cards,pokemon,pokemon-tcg,sdk,tcg,trading-cards
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Software Development :: Libraries
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.10
25
+ Requires-Dist: httpx<1,>=0.27
26
+ Requires-Dist: typing-extensions>=4.5; python_version < '3.11'
27
+ Provides-Extra: dev
28
+ Requires-Dist: build; extra == 'dev'
29
+ Requires-Dist: mypy>=1.11; extra == 'dev'
30
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
31
+ Requires-Dist: pytest>=8; extra == 'dev'
32
+ Requires-Dist: ruff>=0.6; extra == 'dev'
33
+ Requires-Dist: twine; extra == 'dev'
34
+ Description-Content-Type: text/markdown
35
+
36
+ # pokemontcgapi
37
+
38
+ [![PyPI](https://img.shields.io/pypi/v/pokemontcgapi)](https://pypi.org/project/pokemontcgapi/) [![license](https://img.shields.io/pypi/l/pokemontcgapi)](./LICENSE) [![CI](https://github.com/pokemontcgapi/sdk-python/actions/workflows/ci.yml/badge.svg)](https://github.com/pokemontcgapi/sdk-python/actions/workflows/ci.yml)
39
+
40
+ Python client for the Pokémon TCG API at [pokemontcgapi.com](https://pokemontcgapi.com): cards,
41
+ sets, illustrators, the reference vocabularies and photo recognition, across three print lines,
42
+ international, Japanese and Simplified Chinese, with card names in eight locales, images, and prices
43
+ that state their source, basis, grade and sample size. The current counts are live at
44
+ [/v1/status](https://api.pokemontcgapi.com/v1/status).
45
+
46
+ **Every data route has a method**: cards, sets, series, artists, sealed products, the dedicated
47
+ price routes (current, batch, history, stats, movers, sources), the `/v1/changes` feed, the reference
48
+ vocabularies and photo recognition. Account and billing routes (`/v1/me`, keys, checkout) are not
49
+ wrapped: they belong to the dashboard.
50
+
51
+ **One runtime dependency**, [httpx](https://www.python-httpx.org/), which gives the synchronous
52
+ client and the asynchronous one the same request and response objects. Python 3.10 or newer. Fully
53
+ typed (`py.typed`), with `TypedDict` response shapes that never hide a field the API added.
54
+
55
+ Unofficial. Not produced, endorsed, supported by or affiliated with Nintendo, Creatures Inc.,
56
+ GAME FREAK inc. or The Pokémon Company International. Pokémon and all related marks are trademarks of
57
+ their respective owners.
58
+
59
+ ## Get a key
60
+
61
+ Generate the Idempotency-Key once per signup and keep it with the request body:
62
+
63
+ ```bash
64
+ IDEM=$(uuidgen)
65
+ ```
66
+
67
+ ```bash
68
+ curl -s -X POST "https://api.pokemontcgapi.com/v1/accounts/free" \
69
+ -H "Content-Type: application/json" \
70
+ -H "Idempotency-Key: $IDEM" \
71
+ -d '{"email":"you@example.com"}'
72
+ ```
73
+
74
+ Lost the response? Repeat the exact same request (same Idempotency-Key, same body byte for byte, same network: same public IPv4 or the same IPv6 /64) within 24 hours and the response comes back, if stored, secret included; it is the original response, so a key rotated or revoked since then is not revived. A new Idempotency-Key for the same email returns 409 ACCOUNT_EXISTS; the same key with a different body returns 409 IDEMPOTENCY_CONFLICT.
75
+
76
+ We store only a hash of the key; the signup response is kept for 24 hours so the same request can be replayed. Save `data.key.secret` now.
77
+
78
+ If replay is unavailable, [sign in](https://pokemontcgapi.com/account) and rotate the key, or use /v1/accounts/recover with an already verified email to get a new secret.
79
+
80
+ The key comes back in `data.key.secret`. Confirming the address we email raises the trial from
81
+ 80 to 800 credits, and the trial ends 30 days after signup. Paid plans start at 29 EUR a month:
82
+ [pricing](https://pokemontcgapi.com/pricing).
83
+
84
+ ## Install
85
+
86
+ ```bash
87
+ pip install pokemontcgapi
88
+ ```
89
+
90
+ ## Use
91
+
92
+ ```python
93
+ from pokemontcgapi import PokemonTcgApi
94
+
95
+ client = PokemonTcgApi() # reads PTCG_API_KEY from the environment; or PokemonTcgApi("your-key")
96
+
97
+ card = client.cards.get("base1-4", include=["prices"])
98
+ print(card["id"], card["name"], card.get("index_eur"))
99
+ # bs-4 Charizard 523.76 ← the index on 16 September 2026; it moves, yours will differ
100
+ ```
101
+
102
+ `base1-4` and `bs-4` both resolve: the id is the printed coordinate — set code, dash, collector
103
+ number — and the alternate legacy id resolves on the same route, so a catalogue you already have does
104
+ not start with a matching problem.
105
+
106
+ Responses are the decoded JSON, typed as `TypedDict`: read `card["name"]` as in the documentation,
107
+ and `card.get("index_eur")` for the keys that are only present with an `include`. The same client
108
+ exists as `AsyncPokemonTcgApi`, with the same methods to `await` and pages to `async for` over:
109
+
110
+ ```python
111
+ from pokemontcgapi import AsyncPokemonTcgApi
112
+
113
+ async with AsyncPokemonTcgApi() as client:
114
+ card = await client.cards.get("base1-4")
115
+ async for set_ in await client.sets.list(region="JP"):
116
+ print(set_["code"], set_["name"])
117
+ ```
118
+
119
+ ### Pagination that you never have to think about
120
+
121
+ Every list method returns a `Page`, which is also iterable. Iterating it follows
122
+ `links.next` for you. For an initial import of all cards, use the flat card list so pages fill
123
+ across set boundaries:
124
+
125
+ ```python
126
+ for card in client.cards.search(limit=250, order_by="id"):
127
+ print(card["id"], card["name"], card["set_code"])
128
+ ```
129
+
130
+ For Japanese cards, add `q="set.region:JP"`; for Simplified Chinese cards, use
131
+ `q="set.region:CN"`. Add `include=["translations"]` when you need localized names;
132
+ this keeps the plain catalogue cost. `include=["index"]` and `include=["prices"]`
133
+ have different credit costs. `lang` selects a name translation, not a print region.
134
+
135
+ Use `client.sets.list(region="JP", limit=250)` to browse set metadata and
136
+ `client.sets.cards("obf", limit=250)` when you need one particular set. For all cards,
137
+ the flat list uses fewer requests than a card loop for every set. The
138
+ [quickstart](https://pokemontcgapi.com/docs/quickstart#page-the-whole-catalogue) includes dated
139
+ measurements, and the [migration guide](https://pokemontcgapi.com/docs/migrate-from-pokemontcg-io)
140
+ explains capturing the change feed watermark before an import and keeping the replica current.
141
+
142
+ The cursor carries a signature of the sort order, so it must never be reconstructed by hand — the SDK
143
+ follows the URL the API returned, which is the failure mode this avoids. `page.next_page()` gives you
144
+ one page at a time, `page.pages()` every page, and `.to_list(max=...)` requires an explicit
145
+ ceiling, because the catalogue is large enough that an unbounded materialisation is a mistake rather
146
+ than a choice.
147
+
148
+ ### One call for a hundred cards
149
+
150
+ ```python
151
+ result = client.cards.batch(
152
+ ["sv8-116", "sv8-100", "inventato-xyz"],
153
+ include=["index"], # index_eur on list and batch rows is opt-in: 1 credit per 50 cards
154
+ select=["id", "name", "index_eur"],
155
+ )
156
+ data, requested, found = result["data"], result["requested"], result["found"]
157
+ missing = result.get("missing", [])
158
+ ```
159
+
160
+ `missing` is optional: it is absent when every id resolves. Otherwise each unresolved id appears once
161
+ as `{"id": ..., "suggested_id": ...}`, the suggestion only for an existing historical candidate in a
162
+ different canonical set. In this example only `sv8-100` is returned; `sv8-116` suggests `ssp-116`,
163
+ and `inventato-xyz` has no suggestion. A canonical set prefix binds the lookup to that set;
164
+ `base1-4` still resolves to `bs-4` because `base1` is only a historical alias.
165
+
166
+ `data` contains distinct cards. Repeated ids count towards `requested` and credits, but do not
167
+ repeat rows in `data` or `missing`. Two valid aliases for one card can make `found` smaller than
168
+ `requested` with no missing ids. Missing entries ignore case and retain the first spelling and
169
+ request order after whitespace trimming. `withheld` remains an optional top-level key. More than
170
+ 100 ids raise `ValueError` before any request: chunk the list.
171
+
172
+ ### Japanese, and the other seven locales
173
+
174
+ ```python
175
+ page = client.sets.cards("sv8", lang="ja", limit=1)
176
+ print(page.data[0]["name"]) # タマタマ
177
+ ```
178
+
179
+ `lang` replaces the `name` field itself and falls back to English where a translation is missing.
180
+ Locales, with the rows each one actually has on 16 September 2026: `en` 57,421, `fr` 42,858,
181
+ `de` 42,604, `ja` 27,230, `it` 21,644, `es` 21,003, `pt` 13,822, `zh` 3,492. A thin locale answers
182
+ mostly in English, because the fallback is per card and not per request.
183
+
184
+ ### Conditional requests are free
185
+
186
+ ```python
187
+ client = PokemonTcgApi(cache="etag")
188
+ ```
189
+
190
+ Every collection carries an ETag. We compute it strong, from the body; the edge rewrites it weak with
191
+ an encoding suffix when it compresses, so what you receive looks like `W/"…-gzip"` and you send back
192
+ exactly that. With the cache on, the client stores it and replays a `304` without a body, and a `304`
193
+ consumes no quota. A mirror that re-syncs often pays only for what changed.
194
+
195
+ ### A photo instead of an id
196
+
197
+ **Included from the Growth plan up.** On a trial or a Developer key the call answers `403
198
+ PLAN_REQUIRED` with `details.min_plan`, before reading the image and without spending credits.
199
+
200
+ ```python
201
+ result = client.vision.identify("photo.jpg", set="sv3") # a path, bytes or an open binary file
202
+ data = result["data"]
203
+
204
+ # Read `decision` before `id`. Always.
205
+ if data["decision"] == "match":
206
+ # One candidate, close, and clear of the next.
207
+ add(data["id"])
208
+ elif data["decision"] == "ambiguous":
209
+ # Two printings share this illustration. `data["id"]` is None on purpose.
210
+ show_picker(data["candidates"])
211
+ else:
212
+ ask_for_a_better_photo()
213
+ ```
214
+
215
+ Reprints and regional twins share their artwork, so artwork alone cannot name a printing — not here
216
+ and not anywhere. The endpoint returns candidates with a `distance` (0–512, lower is closer; real
217
+ matches land well under 150) and refuses to pick when two are within a few bits of each other.
218
+ Passing `set` or `region` when your workflow knows them is what resolves the tie.
219
+
220
+ It costs 25 credits a call against 1 for a lookup: it is the whole image index answering, not a row
221
+ being read. Do not put it in a loop.
222
+
223
+ ### Errors you can branch on
224
+
225
+ ```python
226
+ from pokemontcgapi import NotFoundError, QuotaExceededError, RateLimitedError
227
+
228
+ try:
229
+ client.cards.get("nope-1")
230
+ except NotFoundError:
231
+ ...
232
+ except RateLimitedError as error:
233
+ ... # error.retry_after
234
+ except QuotaExceededError:
235
+ ... # retrying will never help
236
+ ```
237
+
238
+ Every error carries `code`, `status`, `details` and `request_id` — quote the request id in a support
239
+ message, it is the only thing that can be looked up. Retries use exponential backoff with full
240
+ jitter on 429, 5xx and network failures, honour `Retry-After`, and never retry a quota exhaustion.
241
+ Network failures raise `ApiConnectionError` (and `ApiTimeoutError` after the per-attempt `timeout`),
242
+ which are not API errors and carry no code.
243
+
244
+ Commercial refusals include `details.next_step`, exposed as the typed `error.next_step`. If `error.next_step` exists, show `error.next_step["handoff"]` and its URL to the account owner verbatim and do not retry. `error.action_url` returns the URL for any action: `checkout_url` for subscribe, `manage_url` for upgrade, `verify_url` for email verification, or `contact_url` for sales and support. Show it alongside `error.handoff`. Upgrades point to the account page, where the owner opens the billing portal to change plan. `error.checkout_url` remains a shortcut for subscribe only.
245
+
246
+ ```python
247
+ except PokemonTcgApiError as error:
248
+ if error.next_step:
249
+ show_to_user(error.next_step["handoff"])
250
+ ```
251
+
252
+ ## What this API does not have
253
+
254
+ Stated up front so you find out here rather than three days into an integration:
255
+
256
+ - **No Korean cards.** Zero `KR` sets, zero `ko` translations. Both are modelled in the schema and
257
+ carry no data.
258
+ - **Card game text is English, and uneven.** `attacks`, `abilities`, `weaknesses`, `resistances`,
259
+ `subtypes`, `retreat_cost`, `rules` and `flavor_text` carry rows since 3 September 2026, on the
260
+ 20,725 Western printings. Measured on 16 September 2026 against 57,450 cards: `attacks` on 29.9% of
261
+ the whole catalogue and 82.9% of the Western part, `subtypes` 35.0%, `abilities` 7.0%.
262
+ Japanese and Chinese printings carry none. The types in this package keep them `| None`, so the
263
+ type checker makes you handle the part that is absent.
264
+ - **No format legalities.** The card object has no `legalities` field and `include` rejects the
265
+ value with a 400. If you are building a deck checker, this is not the data source you need.
266
+
267
+ What it does have: the printing itself — set, number, rarity, region, release date, illustrator,
268
+ image, marketplace ids, names in eight locales — and prices.
269
+
270
+ ## Prices
271
+
272
+ ```python
273
+ card = client.cards.get("base1-4", include=["prices"])
274
+ for price in card.get("prices", []):
275
+ print(price["source"], price["basis"], price["amount"], price["currency"], price["as_of"], price["sample_n"])
276
+ ```
277
+
278
+ The dedicated price routes have their own methods, and they are the ones to use when prices are the
279
+ point of the call:
280
+
281
+ ```python
282
+ prices = client.prices.card("base1-4") # index + quotes, 2 credits
283
+ many = client.prices.current(["base1-4", "sv3-125"]) # up to 50 ids, 4 credits per 25
284
+ history = client.prices.history("base1-4", bucket="week") # 5 credits
285
+ stats = client.prices.stats("base1-4", window="30d") # 2 credits
286
+ movers = client.prices.movers(window="7d", direction="gainers") # Growth and up
287
+ box = client.sealed.prices("evolving-skies-booster-box")
288
+ ```
289
+
290
+ `history` is bounded by your plan (7 days on the trial, 30 on Developer, everything from Growth): a
291
+ wider window raises `UpgradeRequiredError`, whose `permitted_window` says what you may ask for. Its
292
+ date bounds are `from_` and `to` (`from` is a reserved word in Python; it is sent as `from`).
293
+ `movers` below Growth raises `PlanRequiredError`, and a trial past its 30 days raises
294
+ `TrialExpiredError` on every route that costs credits. Both extend `PermissionDeniedError`.
295
+
296
+ There is no printing filter on `include=["prices"]`: first edition, holofoil and graded rows come back together, so read
297
+ `printing`, `condition` and `grading` per row. `basis` separates `GUIDE` (published upstream) from
298
+ `DERIVED` (computed by us). `PTCG_INDEX` is a composite index in EUR carrying `sample_n`, and the same
299
+ number sits on the card row as `index_eur` wherever we have enough observations to compute one: 51,636
300
+ cards of 57,450 on 16 September 2026, so treat it as nullable. On a list or batch it comes with `include=["index"]`
301
+ (1 credit per 50 rows), so a list still has a comparable number without a second request per card.
302
+
303
+ What your plan withholds is named rather than hidden, but it is named in three different places, so
304
+ read the one that matches the call you made:
305
+
306
+ | call | where the exclusions are |
307
+ |---|---|
308
+ | `client.prices.card(id)` | `["meta"]["withheld"]` |
309
+ | `client.cards.get(id, include=["prices"])` | the `X-Plan-Withheld` header: `client.last_response.plan_withheld` |
310
+ | `client.cards.batch(ids, …)` | a top-level `withheld` key |
311
+
312
+ The values are `graded` and `non_english_locales`: a trial key gets both, Developer keeps `graded`,
313
+ and from Growth up nothing is withheld, in which case the key is absent rather than an empty list.
314
+ Read it before concluding that a card has no graded observations: it may be your plan, not the
315
+ catalogue. Prices also carry their own `locale`, and a card read with `include=["prices"]` returns
316
+ every locale your plan allows, so the currency does not tell you the language.
317
+
318
+ ## Credits and quota
319
+
320
+ Every response says what it cost. The SDK keeps the headers of the last one, and hands each one to
321
+ `on_response` if you want a running total:
322
+
323
+ ```python
324
+ spent = 0
325
+
326
+ def count(info):
327
+ global spent
328
+ spent += info.credits_cost or 0
329
+
330
+ client = PokemonTcgApi(on_response=count)
331
+
332
+ client.cards.search(q="name:charizard", include=["index"])
333
+ print(client.last_response.credits_cost, client.last_response.quota_remaining)
334
+ ```
335
+
336
+ The trial is 800 credits, once, for 30 days, with at most 400 spent in a day; `trial_expires_at` on
337
+ the same object says when it ends.
338
+
339
+ ## The change feed
340
+
341
+ ```python
342
+ since = int(store.get("ptcg_since") or 0)
343
+ while True:
344
+ page = client.changes(since=since, limit=500)
345
+ for change in page["data"]:
346
+ apply(change) # kind, entity_id, op, version
347
+ since = page["meta"]["next_since"]
348
+ store.set("ptcg_since", since)
349
+ if not page["meta"]["has_more"]:
350
+ break
351
+ ```
352
+
353
+ ## Also available
354
+
355
+ - **TypeScript SDK**: [`@pokemontcgapi/sdk`](https://www.npmjs.com/package/@pokemontcgapi/sdk) — [source](https://github.com/pokemontcgapi/sdk-typescript)
356
+ - **Go SDK**: `go get github.com/pokemontcgapi/sdk-go` — [source](https://github.com/pokemontcgapi/sdk-go)
357
+ - **MCP server** for agents: [`@pokemontcgapi/mcp`](https://www.npmjs.com/package/@pokemontcgapi/mcp) — [source](https://github.com/pokemontcgapi/mcp-server)
358
+ - **Docs**: <https://pokemontcgapi.com/docs>
359
+ - **Coverage, measured live**: <https://pokemontcgapi.com/coverage>
360
+
361
+ ## Build from source
362
+
363
+ ```bash
364
+ python -m venv .venv && . .venv/bin/activate # .venv\Scripts\activate on Windows
365
+ pip install -e .[dev]
366
+ ruff check src tests && mypy src && pytest
367
+ python -m build
368
+ ```
369
+
370
+ Python >= 3.10. `pytest` runs the offline suite in `tests/` against the recorded responses in
371
+ `tests/fixtures/`; `PTCG_LIVE=1 PTCG_API_KEY=... python scripts/smoke_live.py` runs a short check
372
+ against the real API (about four credits). CI enforces lint, types and the suite on every supported
373
+ Python, and that the built wheel installs and imports in a clean environment.
374
+
375
+ This package is developed inside the private monorepo that runs
376
+ [pokemontcgapi.com](https://pokemontcgapi.com) and mirrored here on each release,
377
+ so a merged pull request travels back by hand rather than by merge button. That
378
+ is not a reason to send patches elsewhere — open the issue or the PR here, it is
379
+ the address that gets read. The prose of this README is kept identical to the TypeScript SDK's
380
+ (synced from its README as of commit 86a725a); only the code differs.
381
+
382
+ ## Licence
383
+
384
+ MIT. Data served by the API carries per-source redistribution terms — see
385
+ <https://pokemontcgapi.com/legal/attribution>.