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.
@@ -0,0 +1,14 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.egg-info/
5
+ .pytest_cache/
6
+ .mypy_cache/
7
+ .ruff_cache/
8
+ build/
9
+ dist/
10
+ .venv/
11
+ venv/
12
+ .env
13
+ .coverage
14
+ htmlcov/
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
+ [![PyPI](https://img.shields.io/pypi/v/tcgapi.svg)](https://pypi.org/project/tcgapi/)
40
+ [![Python](https://img.shields.io/pypi/pyversions/tcgapi.svg)](https://pypi.org/project/tcgapi/)
41
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
+ [![PyPI](https://img.shields.io/pypi/v/tcgapi.svg)](https://pypi.org/project/tcgapi/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/tcgapi.svg)](https://pypi.org/project/tcgapi/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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 {}