tcgapi 0.2.1__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.
- tcgapi-0.2.1/.gitignore +14 -0
- tcgapi-0.2.1/LICENSE +21 -0
- tcgapi-0.2.1/PKG-INFO +214 -0
- tcgapi-0.2.1/README.md +178 -0
- tcgapi-0.2.1/pyproject.toml +85 -0
- tcgapi-0.2.1/tcgapi/__init__.py +68 -0
- tcgapi-0.2.1/tcgapi/_transport.py +66 -0
- tcgapi-0.2.1/tcgapi/async_client.py +75 -0
- tcgapi-0.2.1/tcgapi/client.py +79 -0
- tcgapi-0.2.1/tcgapi/errors.py +78 -0
- tcgapi-0.2.1/tcgapi/models.py +211 -0
- tcgapi-0.2.1/tcgapi/resources/__init__.py +0 -0
- tcgapi-0.2.1/tcgapi/resources/_base.py +27 -0
- tcgapi-0.2.1/tcgapi/resources/bulk.py +140 -0
- tcgapi-0.2.1/tcgapi/resources/cards.py +88 -0
- tcgapi-0.2.1/tcgapi/resources/export.py +36 -0
- tcgapi-0.2.1/tcgapi/resources/games.py +48 -0
- tcgapi-0.2.1/tcgapi/resources/keys.py +30 -0
- tcgapi-0.2.1/tcgapi/resources/prices.py +74 -0
- tcgapi-0.2.1/tcgapi/resources/search.py +118 -0
- tcgapi-0.2.1/tcgapi/resources/sets.py +115 -0
- tcgapi-0.2.1/tcgapi/resources/usage.py +30 -0
tcgapi-0.2.1/.gitignore
ADDED
tcgapi-0.2.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 tcgapi.dev
|
|
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.
|
tcgapi-0.2.1/PKG-INFO
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tcgapi
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: Official Python SDK for tcgapi.dev — pricing data for Pokemon, Magic: The Gathering, Yu-Gi-Oh!, Lorcana, One Piece, and 80+ more trading card games.
|
|
5
|
+
Project-URL: Homepage, https://tcgapi.dev
|
|
6
|
+
Project-URL: Documentation, https://tcgapi.dev/api/
|
|
7
|
+
Project-URL: Source, https://github.com/gordy-ftw/tcgapi-python
|
|
8
|
+
Project-URL: Issues, https://github.com/gordy-ftw/tcgapi-python/issues
|
|
9
|
+
Project-URL: Quickstart, https://tcgapi.dev/quickstart/
|
|
10
|
+
Author: tcgapi.dev
|
|
11
|
+
License: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: card-prices,flesh-and-blood,lorcana,magic-the-gathering,mtg,one-piece-tcg,pokemon,pokemon-prices,pokemon-tcg,sdk,tcg,tcg-api,tcgapi,tcgplayer,trading-cards,yugioh
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Topic :: Games/Entertainment
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Python: >=3.9
|
|
27
|
+
Requires-Dist: eval-type-backport>=0.2; python_version < '3.10'
|
|
28
|
+
Requires-Dist: httpx>=0.25
|
|
29
|
+
Requires-Dist: pydantic>=2.0
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: mypy>=1.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
34
|
+
Requires-Dist: ruff>=0.1; extra == 'dev'
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
# tcgapi
|
|
38
|
+
|
|
39
|
+
[](https://pypi.org/project/tcgapi/)
|
|
40
|
+
[](https://pypi.org/project/tcgapi/)
|
|
41
|
+
[](https://opensource.org/licenses/MIT)
|
|
42
|
+
|
|
43
|
+
Official Python SDK for [**tcgapi.dev**](https://tcgapi.dev) — a unified pricing API for **89+ trading card games**, including:
|
|
44
|
+
|
|
45
|
+
- Pokémon TCG (English + Japanese)
|
|
46
|
+
- Magic: The Gathering
|
|
47
|
+
- Yu-Gi-Oh!
|
|
48
|
+
- Lorcana
|
|
49
|
+
- One Piece Card Game
|
|
50
|
+
- Flesh and Blood
|
|
51
|
+
- Star Wars Unlimited
|
|
52
|
+
- Digimon, Dragon Ball Super, Riftbound, Union Arena, Final Fantasy TCG, Weiss Schwarz, Cardfight!! Vanguard, and dozens more.
|
|
53
|
+
|
|
54
|
+
Real-time market prices, full price history, fuzzy search, bulk lookups, and exports — all from one HTTP API. Sync and async clients, fully typed with Pydantic.
|
|
55
|
+
|
|
56
|
+
## Install
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install tcgapi
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Requires Python 3.9+.
|
|
63
|
+
|
|
64
|
+
## Quickstart
|
|
65
|
+
|
|
66
|
+
Get a free API key at [**tcgapi.dev/dashboard**](https://tcgapi.dev/dashboard) (100 requests/day, no credit card).
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from tcgapi import TCGApi
|
|
70
|
+
|
|
71
|
+
tcg = TCGApi(api_key="tcg_live_...") # or set TCGAPI_KEY env var
|
|
72
|
+
|
|
73
|
+
# Look up a single card
|
|
74
|
+
card = tcg.cards.get(123456)
|
|
75
|
+
print(card.data.name)
|
|
76
|
+
|
|
77
|
+
# Get every printing's current price
|
|
78
|
+
prices = tcg.cards.prices(123456)
|
|
79
|
+
for p in prices.data:
|
|
80
|
+
print(f"{p.printing}: ${p.market_price}")
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
If `api_key` is omitted, the client reads from `TCGAPI_KEY`.
|
|
84
|
+
|
|
85
|
+
## Examples
|
|
86
|
+
|
|
87
|
+
### Search across every game
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
results = tcg.search.cards(
|
|
91
|
+
"charizard",
|
|
92
|
+
game="pokemon",
|
|
93
|
+
sort="price_desc",
|
|
94
|
+
per_page=20,
|
|
95
|
+
)
|
|
96
|
+
for card in results.data:
|
|
97
|
+
print(card.name, card.set_name, card.market_price)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Iterate without pagination boilerplate
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
for card in tcg.search.iter("lightning", game="magic"):
|
|
104
|
+
# walks meta.has_more automatically — caps at the API's 200/page max
|
|
105
|
+
...
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Browse sets
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
games = tcg.games.list()
|
|
112
|
+
pokemon_sets = tcg.games.sets("pokemon")
|
|
113
|
+
surging_sparks = next(
|
|
114
|
+
(s for s in pokemon_sets.data if "Surging Sparks" in s.name), None
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
if surging_sparks:
|
|
118
|
+
cards = tcg.sets.cards(surging_sparks.id, sort="price_desc")
|
|
119
|
+
print(f"{surging_sparks.name}: {cards.meta.total} cards")
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Bulk price lookup (Pro+)
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
# Auto-chunks if you pass more than 500 IDs.
|
|
126
|
+
bulk = tcg.bulk.prices([1, 2, 3, ...]) # thousands ok
|
|
127
|
+
print(f"Got prices for {len(bulk.data)} card-printings")
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Top movers
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
movers = tcg.prices.top_movers(
|
|
134
|
+
game="pokemon",
|
|
135
|
+
direction="up",
|
|
136
|
+
period="7d",
|
|
137
|
+
limit=10,
|
|
138
|
+
)
|
|
139
|
+
for m in movers.data:
|
|
140
|
+
print(f"{m.name} ({m.set_name}): +{m.price_change}% — ${m.market_price}")
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Price history (Hobby+)
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
# Window scales with your tier: free=7d, hobby=30d, starter=90d, pro/business=full.
|
|
147
|
+
history = tcg.cards.history(123456, range="year")
|
|
148
|
+
for point in history.data:
|
|
149
|
+
print(point.date, point.market_price)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Async client
|
|
153
|
+
|
|
154
|
+
The async API mirrors the sync surface verbatim:
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
import asyncio
|
|
158
|
+
from tcgapi import AsyncTCGApi
|
|
159
|
+
|
|
160
|
+
async def main():
|
|
161
|
+
async with AsyncTCGApi() as tcg:
|
|
162
|
+
resp = await tcg.search.cards("charizard", game="pokemon")
|
|
163
|
+
async for card in tcg.search.iter("dragon", game="magic"):
|
|
164
|
+
print(card.name)
|
|
165
|
+
|
|
166
|
+
asyncio.run(main())
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Rate limits
|
|
170
|
+
|
|
171
|
+
Every successful response carries the live rate-limit budget:
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
resp = tcg.games.list()
|
|
175
|
+
print(resp.rate_limit)
|
|
176
|
+
# RateLimit(daily_limit=10000, daily_remaining=9871, daily_reset='2026-04-29T00:00:00.000Z')
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
When you exceed the daily limit you'll get a typed `RateLimitError`:
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
from tcgapi import RateLimitError
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
tcg.cards.get(123)
|
|
186
|
+
except RateLimitError as err:
|
|
187
|
+
print(f"Hit the limit — retry in {err.retry_after}s")
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Other error classes: `AuthError` (401), `TierError` (403), `NotFoundError` (404), `TcgApiError` (anything else). All extend `Exception`.
|
|
191
|
+
|
|
192
|
+
## Tiers
|
|
193
|
+
|
|
194
|
+
| Plan | Daily requests | History | Bulk endpoints | Sales velocity |
|
|
195
|
+
|------|---------------|---------|----------------|----------------|
|
|
196
|
+
| Free | 100 | 7 days | — | — |
|
|
197
|
+
| Hobby ($9.99/mo) | 1,000 | 30 days | — | — |
|
|
198
|
+
| Starter ($19.99/mo) | 2,500 | 90 days | Limited | — |
|
|
199
|
+
| Pro ($49.99/mo) | 10,000 | Full | Yes | Yes |
|
|
200
|
+
| Business ($99.99/mo) | 50,000 | Full | Yes | Yes |
|
|
201
|
+
|
|
202
|
+
Sales velocity = `sales_volume`, `avg_sales_price`, and `sales_as_of` on price responses (Pro+). The fields are absent below Pro.
|
|
203
|
+
|
|
204
|
+
Or pay per request via [x402](https://tcgapi.dev/api/x402) — no signup, USDC on Base or Solana.
|
|
205
|
+
|
|
206
|
+
## API reference
|
|
207
|
+
|
|
208
|
+
Full endpoint reference: [**tcgapi.dev/api**](https://tcgapi.dev/api/)
|
|
209
|
+
OpenAPI spec: [**tcgapi.dev/openapi.yaml**](https://tcgapi.dev/openapi.yaml)
|
|
210
|
+
Quickstart guide: [**tcgapi.dev/quickstart**](https://tcgapi.dev/quickstart/)
|
|
211
|
+
|
|
212
|
+
## License
|
|
213
|
+
|
|
214
|
+
MIT
|
tcgapi-0.2.1/README.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# tcgapi
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/tcgapi/)
|
|
4
|
+
[](https://pypi.org/project/tcgapi/)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
Official Python SDK for [**tcgapi.dev**](https://tcgapi.dev) — a unified pricing API for **89+ trading card games**, including:
|
|
8
|
+
|
|
9
|
+
- Pokémon TCG (English + Japanese)
|
|
10
|
+
- Magic: The Gathering
|
|
11
|
+
- Yu-Gi-Oh!
|
|
12
|
+
- Lorcana
|
|
13
|
+
- One Piece Card Game
|
|
14
|
+
- Flesh and Blood
|
|
15
|
+
- Star Wars Unlimited
|
|
16
|
+
- Digimon, Dragon Ball Super, Riftbound, Union Arena, Final Fantasy TCG, Weiss Schwarz, Cardfight!! Vanguard, and dozens more.
|
|
17
|
+
|
|
18
|
+
Real-time market prices, full price history, fuzzy search, bulk lookups, and exports — all from one HTTP API. Sync and async clients, fully typed with Pydantic.
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install tcgapi
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Requires Python 3.9+.
|
|
27
|
+
|
|
28
|
+
## Quickstart
|
|
29
|
+
|
|
30
|
+
Get a free API key at [**tcgapi.dev/dashboard**](https://tcgapi.dev/dashboard) (100 requests/day, no credit card).
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from tcgapi import TCGApi
|
|
34
|
+
|
|
35
|
+
tcg = TCGApi(api_key="tcg_live_...") # or set TCGAPI_KEY env var
|
|
36
|
+
|
|
37
|
+
# Look up a single card
|
|
38
|
+
card = tcg.cards.get(123456)
|
|
39
|
+
print(card.data.name)
|
|
40
|
+
|
|
41
|
+
# Get every printing's current price
|
|
42
|
+
prices = tcg.cards.prices(123456)
|
|
43
|
+
for p in prices.data:
|
|
44
|
+
print(f"{p.printing}: ${p.market_price}")
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
If `api_key` is omitted, the client reads from `TCGAPI_KEY`.
|
|
48
|
+
|
|
49
|
+
## Examples
|
|
50
|
+
|
|
51
|
+
### Search across every game
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
results = tcg.search.cards(
|
|
55
|
+
"charizard",
|
|
56
|
+
game="pokemon",
|
|
57
|
+
sort="price_desc",
|
|
58
|
+
per_page=20,
|
|
59
|
+
)
|
|
60
|
+
for card in results.data:
|
|
61
|
+
print(card.name, card.set_name, card.market_price)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Iterate without pagination boilerplate
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
for card in tcg.search.iter("lightning", game="magic"):
|
|
68
|
+
# walks meta.has_more automatically — caps at the API's 200/page max
|
|
69
|
+
...
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Browse sets
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
games = tcg.games.list()
|
|
76
|
+
pokemon_sets = tcg.games.sets("pokemon")
|
|
77
|
+
surging_sparks = next(
|
|
78
|
+
(s for s in pokemon_sets.data if "Surging Sparks" in s.name), None
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
if surging_sparks:
|
|
82
|
+
cards = tcg.sets.cards(surging_sparks.id, sort="price_desc")
|
|
83
|
+
print(f"{surging_sparks.name}: {cards.meta.total} cards")
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Bulk price lookup (Pro+)
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
# Auto-chunks if you pass more than 500 IDs.
|
|
90
|
+
bulk = tcg.bulk.prices([1, 2, 3, ...]) # thousands ok
|
|
91
|
+
print(f"Got prices for {len(bulk.data)} card-printings")
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Top movers
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
movers = tcg.prices.top_movers(
|
|
98
|
+
game="pokemon",
|
|
99
|
+
direction="up",
|
|
100
|
+
period="7d",
|
|
101
|
+
limit=10,
|
|
102
|
+
)
|
|
103
|
+
for m in movers.data:
|
|
104
|
+
print(f"{m.name} ({m.set_name}): +{m.price_change}% — ${m.market_price}")
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Price history (Hobby+)
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
# Window scales with your tier: free=7d, hobby=30d, starter=90d, pro/business=full.
|
|
111
|
+
history = tcg.cards.history(123456, range="year")
|
|
112
|
+
for point in history.data:
|
|
113
|
+
print(point.date, point.market_price)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Async client
|
|
117
|
+
|
|
118
|
+
The async API mirrors the sync surface verbatim:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
import asyncio
|
|
122
|
+
from tcgapi import AsyncTCGApi
|
|
123
|
+
|
|
124
|
+
async def main():
|
|
125
|
+
async with AsyncTCGApi() as tcg:
|
|
126
|
+
resp = await tcg.search.cards("charizard", game="pokemon")
|
|
127
|
+
async for card in tcg.search.iter("dragon", game="magic"):
|
|
128
|
+
print(card.name)
|
|
129
|
+
|
|
130
|
+
asyncio.run(main())
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Rate limits
|
|
134
|
+
|
|
135
|
+
Every successful response carries the live rate-limit budget:
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
resp = tcg.games.list()
|
|
139
|
+
print(resp.rate_limit)
|
|
140
|
+
# RateLimit(daily_limit=10000, daily_remaining=9871, daily_reset='2026-04-29T00:00:00.000Z')
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
When you exceed the daily limit you'll get a typed `RateLimitError`:
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from tcgapi import RateLimitError
|
|
147
|
+
|
|
148
|
+
try:
|
|
149
|
+
tcg.cards.get(123)
|
|
150
|
+
except RateLimitError as err:
|
|
151
|
+
print(f"Hit the limit — retry in {err.retry_after}s")
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Other error classes: `AuthError` (401), `TierError` (403), `NotFoundError` (404), `TcgApiError` (anything else). All extend `Exception`.
|
|
155
|
+
|
|
156
|
+
## Tiers
|
|
157
|
+
|
|
158
|
+
| Plan | Daily requests | History | Bulk endpoints | Sales velocity |
|
|
159
|
+
|------|---------------|---------|----------------|----------------|
|
|
160
|
+
| Free | 100 | 7 days | — | — |
|
|
161
|
+
| Hobby ($9.99/mo) | 1,000 | 30 days | — | — |
|
|
162
|
+
| Starter ($19.99/mo) | 2,500 | 90 days | Limited | — |
|
|
163
|
+
| Pro ($49.99/mo) | 10,000 | Full | Yes | Yes |
|
|
164
|
+
| Business ($99.99/mo) | 50,000 | Full | Yes | Yes |
|
|
165
|
+
|
|
166
|
+
Sales velocity = `sales_volume`, `avg_sales_price`, and `sales_as_of` on price responses (Pro+). The fields are absent below Pro.
|
|
167
|
+
|
|
168
|
+
Or pay per request via [x402](https://tcgapi.dev/api/x402) — no signup, USDC on Base or Solana.
|
|
169
|
+
|
|
170
|
+
## API reference
|
|
171
|
+
|
|
172
|
+
Full endpoint reference: [**tcgapi.dev/api**](https://tcgapi.dev/api/)
|
|
173
|
+
OpenAPI spec: [**tcgapi.dev/openapi.yaml**](https://tcgapi.dev/openapi.yaml)
|
|
174
|
+
Quickstart guide: [**tcgapi.dev/quickstart**](https://tcgapi.dev/quickstart/)
|
|
175
|
+
|
|
176
|
+
## License
|
|
177
|
+
|
|
178
|
+
MIT
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tcgapi"
|
|
7
|
+
version = "0.2.1"
|
|
8
|
+
description = "Official Python SDK for tcgapi.dev — pricing data for Pokemon, Magic: The Gathering, Yu-Gi-Oh!, Lorcana, One Piece, and 80+ more trading card games."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "tcgapi.dev" }]
|
|
13
|
+
keywords = [
|
|
14
|
+
"tcg",
|
|
15
|
+
"tcgplayer",
|
|
16
|
+
"pokemon",
|
|
17
|
+
"pokemon-tcg",
|
|
18
|
+
"pokemon-prices",
|
|
19
|
+
"magic-the-gathering",
|
|
20
|
+
"mtg",
|
|
21
|
+
"yugioh",
|
|
22
|
+
"lorcana",
|
|
23
|
+
"one-piece-tcg",
|
|
24
|
+
"flesh-and-blood",
|
|
25
|
+
"card-prices",
|
|
26
|
+
"tcg-api",
|
|
27
|
+
"tcgapi",
|
|
28
|
+
"trading-cards",
|
|
29
|
+
"sdk",
|
|
30
|
+
]
|
|
31
|
+
classifiers = [
|
|
32
|
+
"Development Status :: 4 - Beta",
|
|
33
|
+
"Intended Audience :: Developers",
|
|
34
|
+
"License :: OSI Approved :: MIT License",
|
|
35
|
+
"Programming Language :: Python :: 3",
|
|
36
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
37
|
+
"Programming Language :: Python :: 3.9",
|
|
38
|
+
"Programming Language :: Python :: 3.10",
|
|
39
|
+
"Programming Language :: Python :: 3.11",
|
|
40
|
+
"Programming Language :: Python :: 3.12",
|
|
41
|
+
"Topic :: Games/Entertainment",
|
|
42
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
43
|
+
"Typing :: Typed",
|
|
44
|
+
]
|
|
45
|
+
dependencies = [
|
|
46
|
+
"httpx>=0.25",
|
|
47
|
+
"pydantic>=2.0",
|
|
48
|
+
# Lets pydantic evaluate `int | None` (PEP 604) annotations on Python 3.9,
|
|
49
|
+
# where the `|` union operator isn't available at runtime.
|
|
50
|
+
"eval_type_backport>=0.2; python_version < '3.10'",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
[project.urls]
|
|
54
|
+
Homepage = "https://tcgapi.dev"
|
|
55
|
+
Documentation = "https://tcgapi.dev/api/"
|
|
56
|
+
Source = "https://github.com/gordy-ftw/tcgapi-python"
|
|
57
|
+
Issues = "https://github.com/gordy-ftw/tcgapi-python/issues"
|
|
58
|
+
Quickstart = "https://tcgapi.dev/quickstart/"
|
|
59
|
+
|
|
60
|
+
[project.optional-dependencies]
|
|
61
|
+
dev = [
|
|
62
|
+
"pytest>=7.0",
|
|
63
|
+
"pytest-asyncio>=0.21",
|
|
64
|
+
"ruff>=0.1",
|
|
65
|
+
"mypy>=1.0",
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
[tool.hatch.build.targets.wheel]
|
|
69
|
+
packages = ["tcgapi"]
|
|
70
|
+
|
|
71
|
+
[tool.hatch.build.targets.sdist]
|
|
72
|
+
include = ["/tcgapi", "/README.md", "/LICENSE"]
|
|
73
|
+
|
|
74
|
+
[tool.ruff]
|
|
75
|
+
line-length = 110
|
|
76
|
+
target-version = "py39"
|
|
77
|
+
|
|
78
|
+
[tool.ruff.lint]
|
|
79
|
+
select = ["E", "F", "I", "W", "B", "UP"]
|
|
80
|
+
# UP037: keep string-quoted TYPE_CHECKING refs even with `from __future__ import annotations`
|
|
81
|
+
# UP006/UP035: keep typing.List/Dict generic forms readable on 3.9
|
|
82
|
+
ignore = ["E501", "UP037", "UP006", "UP035"]
|
|
83
|
+
|
|
84
|
+
[tool.pytest.ini_options]
|
|
85
|
+
asyncio_mode = "auto"
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Official Python SDK for tcgapi.dev.
|
|
2
|
+
|
|
3
|
+
>>> from tcgapi import TCGApi
|
|
4
|
+
>>> tcg = TCGApi(api_key="tcg_live_...")
|
|
5
|
+
>>> tcg.games.get("pokemon").data.name
|
|
6
|
+
'Pokemon'
|
|
7
|
+
|
|
8
|
+
Async variant:
|
|
9
|
+
|
|
10
|
+
>>> from tcgapi import AsyncTCGApi
|
|
11
|
+
>>> async with AsyncTCGApi() as tcg:
|
|
12
|
+
... resp = await tcg.search.cards("charizard")
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from .async_client import AsyncTCGApi
|
|
16
|
+
from .client import TCGApi
|
|
17
|
+
from .errors import (
|
|
18
|
+
AuthError,
|
|
19
|
+
NotFoundError,
|
|
20
|
+
RateLimitError,
|
|
21
|
+
TcgApiError,
|
|
22
|
+
TierError,
|
|
23
|
+
)
|
|
24
|
+
from .models import (
|
|
25
|
+
ApiKeyCreated,
|
|
26
|
+
ApiKeySummary,
|
|
27
|
+
BulkCard,
|
|
28
|
+
BulkPriceRow,
|
|
29
|
+
Card,
|
|
30
|
+
CardWithPrice,
|
|
31
|
+
Game,
|
|
32
|
+
Meta,
|
|
33
|
+
Price,
|
|
34
|
+
PriceHistoryPoint,
|
|
35
|
+
PriceMover,
|
|
36
|
+
RateLimit,
|
|
37
|
+
Response,
|
|
38
|
+
Set,
|
|
39
|
+
UsageResponse,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
__version__ = "0.2.1"
|
|
43
|
+
__all__ = [
|
|
44
|
+
"TCGApi",
|
|
45
|
+
"AsyncTCGApi",
|
|
46
|
+
# errors
|
|
47
|
+
"TcgApiError",
|
|
48
|
+
"AuthError",
|
|
49
|
+
"TierError",
|
|
50
|
+
"NotFoundError",
|
|
51
|
+
"RateLimitError",
|
|
52
|
+
# models
|
|
53
|
+
"Game",
|
|
54
|
+
"Set",
|
|
55
|
+
"Card",
|
|
56
|
+
"CardWithPrice",
|
|
57
|
+
"Price",
|
|
58
|
+
"PriceMover",
|
|
59
|
+
"PriceHistoryPoint",
|
|
60
|
+
"BulkCard",
|
|
61
|
+
"BulkPriceRow",
|
|
62
|
+
"ApiKeySummary",
|
|
63
|
+
"ApiKeyCreated",
|
|
64
|
+
"UsageResponse",
|
|
65
|
+
"Meta",
|
|
66
|
+
"RateLimit",
|
|
67
|
+
"Response",
|
|
68
|
+
]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Shared HTTP transport for sync + async clients."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from .errors import make_error
|
|
11
|
+
|
|
12
|
+
DEFAULT_BASE_URL = "https://api.tcgapi.dev/v1"
|
|
13
|
+
DEFAULT_TIMEOUT = 30.0
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class _BaseTransport:
|
|
17
|
+
"""Common transport plumbing — header construction, error handling."""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
api_key: str | None = None,
|
|
22
|
+
base_url: str | None = None,
|
|
23
|
+
timeout: float | None = None,
|
|
24
|
+
user_agent: str | None = None,
|
|
25
|
+
) -> None:
|
|
26
|
+
self.api_key = api_key if api_key is not None else os.environ.get("TCGAPI_KEY")
|
|
27
|
+
self.base_url = (base_url or DEFAULT_BASE_URL).rstrip("/")
|
|
28
|
+
self.timeout = timeout if timeout is not None else DEFAULT_TIMEOUT
|
|
29
|
+
self.user_agent = user_agent
|
|
30
|
+
|
|
31
|
+
def _headers(self) -> dict[str, str]:
|
|
32
|
+
headers = {"Accept": "application/json"}
|
|
33
|
+
if self.api_key:
|
|
34
|
+
headers["X-API-Key"] = self.api_key
|
|
35
|
+
if self.user_agent:
|
|
36
|
+
headers["User-Agent"] = self.user_agent
|
|
37
|
+
return headers
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def _clean_params(params: dict[str, Any] | None) -> dict[str, Any] | None:
|
|
41
|
+
if not params:
|
|
42
|
+
return None
|
|
43
|
+
return {k: v for k, v in params.items() if v is not None}
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def _handle_response(resp: httpx.Response) -> dict[str, Any]:
|
|
47
|
+
try:
|
|
48
|
+
body: dict[str, Any] | None = resp.json() if resp.content else None
|
|
49
|
+
except ValueError:
|
|
50
|
+
body = None
|
|
51
|
+
|
|
52
|
+
if resp.is_error:
|
|
53
|
+
err = (body or {}).get("error") if body else None
|
|
54
|
+
# Standard tcgapi: error = {message, code}. x402 402: error = "string message".
|
|
55
|
+
if isinstance(err, dict):
|
|
56
|
+
message = err.get("message") or f"Request failed with status {resp.status_code}"
|
|
57
|
+
code = err.get("code")
|
|
58
|
+
elif isinstance(err, str):
|
|
59
|
+
message = err
|
|
60
|
+
code = None
|
|
61
|
+
else:
|
|
62
|
+
message = f"Request failed with status {resp.status_code}"
|
|
63
|
+
code = None
|
|
64
|
+
raise make_error(resp.status_code, message, code, body, resp.headers.get("Retry-After"))
|
|
65
|
+
|
|
66
|
+
return body or {}
|