tcgapi 0.2.1__py3-none-any.whl
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/__init__.py +68 -0
- tcgapi/_transport.py +66 -0
- tcgapi/async_client.py +75 -0
- tcgapi/client.py +79 -0
- tcgapi/errors.py +78 -0
- tcgapi/models.py +211 -0
- tcgapi/resources/__init__.py +0 -0
- tcgapi/resources/_base.py +27 -0
- tcgapi/resources/bulk.py +140 -0
- tcgapi/resources/cards.py +88 -0
- tcgapi/resources/export.py +36 -0
- tcgapi/resources/games.py +48 -0
- tcgapi/resources/keys.py +30 -0
- tcgapi/resources/prices.py +74 -0
- tcgapi/resources/search.py +118 -0
- tcgapi/resources/sets.py +115 -0
- tcgapi/resources/usage.py +30 -0
- tcgapi-0.2.1.dist-info/METADATA +214 -0
- tcgapi-0.2.1.dist-info/RECORD +21 -0
- tcgapi-0.2.1.dist-info/WHEEL +4 -0
- tcgapi-0.2.1.dist-info/licenses/LICENSE +21 -0
tcgapi/__init__.py
ADDED
|
@@ -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
|
+
]
|
tcgapi/_transport.py
ADDED
|
@@ -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 {}
|
tcgapi/async_client.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Async TCGApi client. Mirror of the sync client surface."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from ._transport import _BaseTransport
|
|
10
|
+
from .resources.bulk import AsyncBulkResource
|
|
11
|
+
from .resources.cards import AsyncCardsResource
|
|
12
|
+
from .resources.export import AsyncExportResource
|
|
13
|
+
from .resources.games import AsyncGamesResource
|
|
14
|
+
from .resources.keys import AsyncKeysResource
|
|
15
|
+
from .resources.prices import AsyncPricesResource
|
|
16
|
+
from .resources.search import AsyncSearchResource
|
|
17
|
+
from .resources.sets import AsyncSetsResource
|
|
18
|
+
from .resources.usage import AsyncUsageResource
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AsyncTCGApi(_BaseTransport):
|
|
22
|
+
"""Asynchronous client. Backed by httpx.AsyncClient."""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
api_key: str | None = None,
|
|
27
|
+
*,
|
|
28
|
+
base_url: str | None = None,
|
|
29
|
+
timeout: float | None = None,
|
|
30
|
+
user_agent: str | None = None,
|
|
31
|
+
client: httpx.AsyncClient | None = None,
|
|
32
|
+
) -> None:
|
|
33
|
+
super().__init__(api_key=api_key, base_url=base_url, timeout=timeout, user_agent=user_agent)
|
|
34
|
+
self._client = client or httpx.AsyncClient(timeout=self.timeout)
|
|
35
|
+
self._owns_client = client is None
|
|
36
|
+
|
|
37
|
+
self.games = AsyncGamesResource(self)
|
|
38
|
+
self.sets = AsyncSetsResource(self)
|
|
39
|
+
self.cards = AsyncCardsResource(self)
|
|
40
|
+
self.search = AsyncSearchResource(self)
|
|
41
|
+
self.prices = AsyncPricesResource(self)
|
|
42
|
+
self.bulk = AsyncBulkResource(self)
|
|
43
|
+
self.export = AsyncExportResource(self)
|
|
44
|
+
self.keys = AsyncKeysResource(self)
|
|
45
|
+
self.usage = AsyncUsageResource(self)
|
|
46
|
+
|
|
47
|
+
async def _request(self, method: str, path: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
48
|
+
resp = await self._client.request(
|
|
49
|
+
method,
|
|
50
|
+
self.base_url + path,
|
|
51
|
+
params=self._clean_params(params),
|
|
52
|
+
headers=self._headers(),
|
|
53
|
+
)
|
|
54
|
+
return self._handle_response(resp)
|
|
55
|
+
|
|
56
|
+
async def _request_raw(self, method: str, path: str, params: dict[str, Any] | None = None) -> str:
|
|
57
|
+
resp = await self._client.request(
|
|
58
|
+
method,
|
|
59
|
+
self.base_url + path,
|
|
60
|
+
params=self._clean_params(params),
|
|
61
|
+
headers=self._headers(),
|
|
62
|
+
)
|
|
63
|
+
if resp.is_error:
|
|
64
|
+
self._handle_response(resp) # raises
|
|
65
|
+
return resp.text
|
|
66
|
+
|
|
67
|
+
async def close(self) -> None:
|
|
68
|
+
if self._owns_client:
|
|
69
|
+
await self._client.aclose()
|
|
70
|
+
|
|
71
|
+
async def __aenter__(self) -> "AsyncTCGApi":
|
|
72
|
+
return self
|
|
73
|
+
|
|
74
|
+
async def __aexit__(self, *exc: object) -> None:
|
|
75
|
+
await self.close()
|
tcgapi/client.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Sync TCGApi client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from ._transport import _BaseTransport
|
|
10
|
+
from .resources.bulk import BulkResource
|
|
11
|
+
from .resources.cards import CardsResource
|
|
12
|
+
from .resources.export import ExportResource
|
|
13
|
+
from .resources.games import GamesResource
|
|
14
|
+
from .resources.keys import KeysResource
|
|
15
|
+
from .resources.prices import PricesResource
|
|
16
|
+
from .resources.search import SearchResource
|
|
17
|
+
from .resources.sets import SetsResource
|
|
18
|
+
from .resources.usage import UsageResource
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TCGApi(_BaseTransport):
|
|
22
|
+
"""Synchronous client. Backed by httpx.Client.
|
|
23
|
+
|
|
24
|
+
Get a key at https://tcgapi.dev/dashboard. If `api_key` is omitted,
|
|
25
|
+
the client reads from the `TCGAPI_KEY` environment variable.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
api_key: str | None = None,
|
|
31
|
+
*,
|
|
32
|
+
base_url: str | None = None,
|
|
33
|
+
timeout: float | None = None,
|
|
34
|
+
user_agent: str | None = None,
|
|
35
|
+
client: httpx.Client | None = None,
|
|
36
|
+
) -> None:
|
|
37
|
+
super().__init__(api_key=api_key, base_url=base_url, timeout=timeout, user_agent=user_agent)
|
|
38
|
+
self._client = client or httpx.Client(timeout=self.timeout)
|
|
39
|
+
self._owns_client = client is None
|
|
40
|
+
|
|
41
|
+
self.games = GamesResource(self)
|
|
42
|
+
self.sets = SetsResource(self)
|
|
43
|
+
self.cards = CardsResource(self)
|
|
44
|
+
self.search = SearchResource(self)
|
|
45
|
+
self.prices = PricesResource(self)
|
|
46
|
+
self.bulk = BulkResource(self)
|
|
47
|
+
self.export = ExportResource(self)
|
|
48
|
+
self.keys = KeysResource(self)
|
|
49
|
+
self.usage = UsageResource(self)
|
|
50
|
+
|
|
51
|
+
def _request(self, method: str, path: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
52
|
+
resp = self._client.request(
|
|
53
|
+
method,
|
|
54
|
+
self.base_url + path,
|
|
55
|
+
params=self._clean_params(params),
|
|
56
|
+
headers=self._headers(),
|
|
57
|
+
)
|
|
58
|
+
return self._handle_response(resp)
|
|
59
|
+
|
|
60
|
+
def _request_raw(self, method: str, path: str, params: dict[str, Any] | None = None) -> str:
|
|
61
|
+
resp = self._client.request(
|
|
62
|
+
method,
|
|
63
|
+
self.base_url + path,
|
|
64
|
+
params=self._clean_params(params),
|
|
65
|
+
headers=self._headers(),
|
|
66
|
+
)
|
|
67
|
+
if resp.is_error:
|
|
68
|
+
self._handle_response(resp) # raises
|
|
69
|
+
return resp.text
|
|
70
|
+
|
|
71
|
+
def close(self) -> None:
|
|
72
|
+
if self._owns_client:
|
|
73
|
+
self._client.close()
|
|
74
|
+
|
|
75
|
+
def __enter__(self) -> "TCGApi":
|
|
76
|
+
return self
|
|
77
|
+
|
|
78
|
+
def __exit__(self, *exc: object) -> None:
|
|
79
|
+
self.close()
|
tcgapi/errors.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Typed exceptions for tcgapi responses."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TcgApiError(Exception):
|
|
9
|
+
"""Base exception for all tcgapi errors."""
|
|
10
|
+
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
message: str,
|
|
14
|
+
status: int,
|
|
15
|
+
code: str | None = None,
|
|
16
|
+
body: dict[str, Any] | None = None,
|
|
17
|
+
) -> None:
|
|
18
|
+
super().__init__(message)
|
|
19
|
+
self.status = status
|
|
20
|
+
self.code = code or f"HTTP_{status}"
|
|
21
|
+
self.body = body
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class AuthError(TcgApiError):
|
|
25
|
+
"""401 — invalid or missing API key."""
|
|
26
|
+
|
|
27
|
+
def __init__(self, message: str = "Invalid or missing API key", body: dict[str, Any] | None = None) -> None:
|
|
28
|
+
super().__init__(message, 401, "UNAUTHORIZED", body)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TierError(TcgApiError):
|
|
32
|
+
"""403 — endpoint requires a higher tier than the current key has."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, message: str = "Tier upgrade required", body: dict[str, Any] | None = None) -> None:
|
|
35
|
+
super().__init__(message, 403, "TIER_REQUIRED", body)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class NotFoundError(TcgApiError):
|
|
39
|
+
"""404 — resource not found."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, message: str = "Resource not found", body: dict[str, Any] | None = None) -> None:
|
|
42
|
+
super().__init__(message, 404, "NOT_FOUND", body)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class RateLimitError(TcgApiError):
|
|
46
|
+
"""429 — daily request limit reached."""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
message: str = "Daily request limit reached",
|
|
51
|
+
retry_after: int | None = None,
|
|
52
|
+
body: dict[str, Any] | None = None,
|
|
53
|
+
) -> None:
|
|
54
|
+
super().__init__(message, 429, "RATE_LIMITED", body)
|
|
55
|
+
self.retry_after = retry_after
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def make_error(
|
|
59
|
+
status: int,
|
|
60
|
+
message: str,
|
|
61
|
+
code: str | None,
|
|
62
|
+
body: dict[str, Any] | None,
|
|
63
|
+
retry_after: str | None,
|
|
64
|
+
) -> TcgApiError:
|
|
65
|
+
if status == 401:
|
|
66
|
+
return AuthError(message, body)
|
|
67
|
+
if status == 403:
|
|
68
|
+
return TierError(message, body)
|
|
69
|
+
if status == 404:
|
|
70
|
+
return NotFoundError(message, body)
|
|
71
|
+
if status == 429:
|
|
72
|
+
ra: int | None
|
|
73
|
+
try:
|
|
74
|
+
ra = int(retry_after) if retry_after else None
|
|
75
|
+
except (TypeError, ValueError):
|
|
76
|
+
ra = None
|
|
77
|
+
return RateLimitError(message, ra, body)
|
|
78
|
+
return TcgApiError(message, status, code, body)
|
tcgapi/models.py
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""Pydantic models for tcgapi responses. Mirrors openapi/spec.yaml."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Generic, TypeVar
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
8
|
+
|
|
9
|
+
T = TypeVar("T")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class _Base(BaseModel):
|
|
13
|
+
model_config = ConfigDict(extra="ignore", populate_by_name=True)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Game(_Base):
|
|
17
|
+
id: int
|
|
18
|
+
name: str
|
|
19
|
+
slug: str
|
|
20
|
+
tcgplayer_id: int
|
|
21
|
+
priority: int | None = None
|
|
22
|
+
set_count: int | None = None
|
|
23
|
+
card_count: int | None = None
|
|
24
|
+
image_url: str | None = None
|
|
25
|
+
logo_url: str | None = None
|
|
26
|
+
last_synced_at: str | None = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Set(_Base):
|
|
30
|
+
id: int
|
|
31
|
+
name: str
|
|
32
|
+
slug: str | None = None
|
|
33
|
+
tcgplayer_id: int
|
|
34
|
+
abbreviation: str | None = None
|
|
35
|
+
release_date: str | None = None
|
|
36
|
+
card_count: int | None = None
|
|
37
|
+
image_url: str | None = None
|
|
38
|
+
set_icon_url: str | None = None
|
|
39
|
+
game_name: str | None = None
|
|
40
|
+
game_slug: str | None = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Card(_Base):
|
|
44
|
+
id: int
|
|
45
|
+
name: str
|
|
46
|
+
clean_name: str | None = None
|
|
47
|
+
number: str | None = None
|
|
48
|
+
rarity: str | None = None
|
|
49
|
+
image_url: str | None = None
|
|
50
|
+
tcgplayer_id: int
|
|
51
|
+
tcgplayer_url: str | None = None
|
|
52
|
+
product_type: str | None = None
|
|
53
|
+
foil_only: int | None = None
|
|
54
|
+
game_name: str | None = None
|
|
55
|
+
game_slug: str | None = None
|
|
56
|
+
set_name: str | None = None
|
|
57
|
+
set_slug: str | None = None
|
|
58
|
+
custom_attributes: dict[str, Any] | None = None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class Price(_Base):
|
|
62
|
+
card_id: int
|
|
63
|
+
printing: str | None = None
|
|
64
|
+
market_price: float | None = None
|
|
65
|
+
low_price: float | None = None
|
|
66
|
+
median_price: float | None = None
|
|
67
|
+
lowest_with_shipping: float | None = None
|
|
68
|
+
buylist_price: float | None = None
|
|
69
|
+
price_change_24h: float | None = None
|
|
70
|
+
price_change_7d: float | None = None
|
|
71
|
+
price_change_30d: float | None = None
|
|
72
|
+
last_updated_at: str | None = None
|
|
73
|
+
# Sales velocity — Pro/Business tiers only; the API omits these below Pro.
|
|
74
|
+
sales_volume: int | None = None
|
|
75
|
+
avg_sales_price: float | None = None
|
|
76
|
+
sales_as_of: str | None = None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class CardWithPrice(_Base):
|
|
80
|
+
id: int
|
|
81
|
+
name: str
|
|
82
|
+
clean_name: str | None = None
|
|
83
|
+
number: str | None = None
|
|
84
|
+
rarity: str | None = None
|
|
85
|
+
tcgplayer_id: int
|
|
86
|
+
product_type: str | None = None
|
|
87
|
+
foil_only: int | None = None
|
|
88
|
+
total_listings: int | None = None
|
|
89
|
+
printing: str | None = None
|
|
90
|
+
market_price: float | None = None
|
|
91
|
+
low_price: float | None = None
|
|
92
|
+
median_price: float | None = None
|
|
93
|
+
lowest_with_shipping: float | None = None
|
|
94
|
+
price_updated_at: str | None = None
|
|
95
|
+
image_url: str | None = None
|
|
96
|
+
game_name: str | None = None
|
|
97
|
+
game_slug: str | None = None
|
|
98
|
+
set_name: str | None = None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class PriceMover(_Base):
|
|
102
|
+
card_id: int
|
|
103
|
+
name: str
|
|
104
|
+
tcgplayer_id: int
|
|
105
|
+
product_type: str | None = None
|
|
106
|
+
foil_only: int | None = None
|
|
107
|
+
set_name: str | None = None
|
|
108
|
+
game_name: str | None = None
|
|
109
|
+
game_slug: str | None = None
|
|
110
|
+
printing: str | None = None
|
|
111
|
+
market_price: float
|
|
112
|
+
price_change: float
|
|
113
|
+
last_updated_at: str | None = None
|
|
114
|
+
image_url: str | None = None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class BulkPriceRow(_Base):
|
|
118
|
+
card_id: int
|
|
119
|
+
name: str | None = None
|
|
120
|
+
tcgplayer_id: int | None = None
|
|
121
|
+
product_type: str | None = None
|
|
122
|
+
foil_only: int | None = None
|
|
123
|
+
printing: str | None = None
|
|
124
|
+
market_price: float | None = None
|
|
125
|
+
low_price: float | None = None
|
|
126
|
+
median_price: float | None = None
|
|
127
|
+
lowest_with_shipping: float | None = None
|
|
128
|
+
buylist_price: float | None = None
|
|
129
|
+
price_change_24h: float | None = None
|
|
130
|
+
price_change_7d: float | None = None
|
|
131
|
+
price_change_30d: float | None = None
|
|
132
|
+
last_updated_at: str | None = None
|
|
133
|
+
image_url: str | None = None
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class BulkCard(Card):
|
|
137
|
+
prices: list[Price] = Field(default_factory=list)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class PriceHistoryPoint(_Base):
|
|
141
|
+
date: str
|
|
142
|
+
printing: str | None = None
|
|
143
|
+
market_price: float | None = None
|
|
144
|
+
low_price: float | None = None
|
|
145
|
+
avg_sales_price: float | None = None
|
|
146
|
+
sales_volume: int | None = None
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class Meta(_Base):
|
|
150
|
+
total: int | None = None
|
|
151
|
+
page: int | None = None
|
|
152
|
+
per_page: int | None = None
|
|
153
|
+
has_more: bool | None = None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class RateLimit(_Base):
|
|
157
|
+
daily_limit: int
|
|
158
|
+
daily_remaining: int
|
|
159
|
+
daily_reset: str
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class ApiKeySummary(_Base):
|
|
163
|
+
id: str
|
|
164
|
+
key_prefix: str
|
|
165
|
+
name: str
|
|
166
|
+
tier: str
|
|
167
|
+
is_active: int
|
|
168
|
+
total_requests: int
|
|
169
|
+
created_at: str
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class ApiKeyCreated(_Base):
|
|
173
|
+
id: str
|
|
174
|
+
key: str
|
|
175
|
+
key_prefix: str
|
|
176
|
+
name: str
|
|
177
|
+
tier: str
|
|
178
|
+
message: str | None = None
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class UsageAccount(_Base):
|
|
182
|
+
today_requests: int
|
|
183
|
+
daily_limit: int
|
|
184
|
+
daily_remaining: int
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class UsageKey(_Base):
|
|
188
|
+
id: str
|
|
189
|
+
key_prefix: str
|
|
190
|
+
name: str
|
|
191
|
+
tier: str
|
|
192
|
+
total_requests: int
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class UsageSubscription(_Base):
|
|
196
|
+
plan: str
|
|
197
|
+
status: str
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class UsageResponse(_Base):
|
|
201
|
+
account: UsageAccount
|
|
202
|
+
keys: list[UsageKey] = Field(default_factory=list)
|
|
203
|
+
subscription: UsageSubscription | None = None
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class Response(_Base, Generic[T]):
|
|
207
|
+
"""Wrapper returned by every method. Reach `.data`, `.meta`, `.rate_limit`."""
|
|
208
|
+
|
|
209
|
+
data: T
|
|
210
|
+
meta: Meta | None = None
|
|
211
|
+
rate_limit: RateLimit | None = None
|
|
File without changes
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Helpers shared by every resource module."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, TypeVar
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel
|
|
8
|
+
|
|
9
|
+
from ..models import Meta, RateLimit, Response
|
|
10
|
+
|
|
11
|
+
T = TypeVar("T")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def parse_response(model: type[T], body: dict[str, Any]) -> Response[T]:
|
|
15
|
+
"""Coerce a raw response body into a typed Response[T] wrapper."""
|
|
16
|
+
raw_data = body.get("data")
|
|
17
|
+
if isinstance(model, type) and issubclass(model, BaseModel):
|
|
18
|
+
data = model.model_validate(raw_data)
|
|
19
|
+
else:
|
|
20
|
+
# Generic types (list[X], dict[str, list[X]], etc.) handled with TypeAdapter via the caller.
|
|
21
|
+
from pydantic import TypeAdapter
|
|
22
|
+
|
|
23
|
+
data = TypeAdapter(model).validate_python(raw_data)
|
|
24
|
+
|
|
25
|
+
meta = Meta.model_validate(body["meta"]) if body.get("meta") else None
|
|
26
|
+
rate_limit = RateLimit.model_validate(body["rate_limit"]) if body.get("rate_limit") else None
|
|
27
|
+
return Response[T](data=data, meta=meta, rate_limit=rate_limit) # type: ignore[valid-type]
|