tcgapi 0.2.1__tar.gz → 0.3.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.
- {tcgapi-0.2.1 → tcgapi-0.3.0}/PKG-INFO +17 -1
- {tcgapi-0.2.1 → tcgapi-0.3.0}/README.md +16 -0
- {tcgapi-0.2.1 → tcgapi-0.3.0}/pyproject.toml +1 -1
- {tcgapi-0.2.1 → tcgapi-0.3.0}/tcgapi/__init__.py +7 -1
- {tcgapi-0.2.1 → tcgapi-0.3.0}/tcgapi/models.py +41 -0
- {tcgapi-0.2.1 → tcgapi-0.3.0}/tcgapi/resources/_base.py +9 -3
- {tcgapi-0.2.1 → tcgapi-0.3.0}/tcgapi/resources/bulk.py +38 -1
- {tcgapi-0.2.1 → tcgapi-0.3.0}/tcgapi/resources/cards.py +19 -1
- {tcgapi-0.2.1 → tcgapi-0.3.0}/.gitignore +0 -0
- {tcgapi-0.2.1 → tcgapi-0.3.0}/LICENSE +0 -0
- {tcgapi-0.2.1 → tcgapi-0.3.0}/tcgapi/_transport.py +0 -0
- {tcgapi-0.2.1 → tcgapi-0.3.0}/tcgapi/async_client.py +0 -0
- {tcgapi-0.2.1 → tcgapi-0.3.0}/tcgapi/client.py +0 -0
- {tcgapi-0.2.1 → tcgapi-0.3.0}/tcgapi/errors.py +0 -0
- {tcgapi-0.2.1 → tcgapi-0.3.0}/tcgapi/resources/__init__.py +0 -0
- {tcgapi-0.2.1 → tcgapi-0.3.0}/tcgapi/resources/export.py +0 -0
- {tcgapi-0.2.1 → tcgapi-0.3.0}/tcgapi/resources/games.py +0 -0
- {tcgapi-0.2.1 → tcgapi-0.3.0}/tcgapi/resources/keys.py +0 -0
- {tcgapi-0.2.1 → tcgapi-0.3.0}/tcgapi/resources/prices.py +0 -0
- {tcgapi-0.2.1 → tcgapi-0.3.0}/tcgapi/resources/search.py +0 -0
- {tcgapi-0.2.1 → tcgapi-0.3.0}/tcgapi/resources/sets.py +0 -0
- {tcgapi-0.2.1 → tcgapi-0.3.0}/tcgapi/resources/usage.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tcgapi
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
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
5
|
Project-URL: Homepage, https://tcgapi.dev
|
|
6
6
|
Project-URL: Documentation, https://tcgapi.dev/api/
|
|
@@ -127,6 +127,22 @@ bulk = tcg.bulk.prices([1, 2, 3, ...]) # thousands ok
|
|
|
127
127
|
print(f"Got prices for {len(bulk.data)} card-printings")
|
|
128
128
|
```
|
|
129
129
|
|
|
130
|
+
### Per-condition prices (Pro+)
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
# Price floors per (printing, condition) — Near Mint, Lightly Played, etc.
|
|
134
|
+
conditions = tcg.cards.conditions(13217, printing="Holofoil")
|
|
135
|
+
for row in conditions.data:
|
|
136
|
+
# median_with_shipping is robust to $0.01 junk listings — prefer it when pricing inventory
|
|
137
|
+
print(f"{row.printing} / {row.condition}: ${row.median_with_shipping}")
|
|
138
|
+
print(conditions.meta.cached, conditions.meta.condition_counts)
|
|
139
|
+
|
|
140
|
+
# Bulk variant — up to 500 cards per call, served from the nightly cache.
|
|
141
|
+
# Cards without condition data yet are absent; fetch them once via
|
|
142
|
+
# tcg.cards.conditions() to warm them.
|
|
143
|
+
bulk_conditions = tcg.bulk.conditions([13217, 13218, 13219])
|
|
144
|
+
```
|
|
145
|
+
|
|
130
146
|
### Top movers
|
|
131
147
|
|
|
132
148
|
```python
|
|
@@ -91,6 +91,22 @@ bulk = tcg.bulk.prices([1, 2, 3, ...]) # thousands ok
|
|
|
91
91
|
print(f"Got prices for {len(bulk.data)} card-printings")
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
+
### Per-condition prices (Pro+)
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
# Price floors per (printing, condition) — Near Mint, Lightly Played, etc.
|
|
98
|
+
conditions = tcg.cards.conditions(13217, printing="Holofoil")
|
|
99
|
+
for row in conditions.data:
|
|
100
|
+
# median_with_shipping is robust to $0.01 junk listings — prefer it when pricing inventory
|
|
101
|
+
print(f"{row.printing} / {row.condition}: ${row.median_with_shipping}")
|
|
102
|
+
print(conditions.meta.cached, conditions.meta.condition_counts)
|
|
103
|
+
|
|
104
|
+
# Bulk variant — up to 500 cards per call, served from the nightly cache.
|
|
105
|
+
# Cards without condition data yet are absent; fetch them once via
|
|
106
|
+
# tcg.cards.conditions() to warm them.
|
|
107
|
+
bulk_conditions = tcg.bulk.conditions([13217, 13218, 13219])
|
|
108
|
+
```
|
|
109
|
+
|
|
94
110
|
### Top movers
|
|
95
111
|
|
|
96
112
|
```python
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tcgapi"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
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
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -25,9 +25,12 @@ from .models import (
|
|
|
25
25
|
ApiKeyCreated,
|
|
26
26
|
ApiKeySummary,
|
|
27
27
|
BulkCard,
|
|
28
|
+
BulkConditionRow,
|
|
28
29
|
BulkPriceRow,
|
|
29
30
|
Card,
|
|
30
31
|
CardWithPrice,
|
|
32
|
+
ConditionMeta,
|
|
33
|
+
ConditionPrice,
|
|
31
34
|
Game,
|
|
32
35
|
Meta,
|
|
33
36
|
Price,
|
|
@@ -39,7 +42,7 @@ from .models import (
|
|
|
39
42
|
UsageResponse,
|
|
40
43
|
)
|
|
41
44
|
|
|
42
|
-
__version__ = "0.
|
|
45
|
+
__version__ = "0.3.0"
|
|
43
46
|
__all__ = [
|
|
44
47
|
"TCGApi",
|
|
45
48
|
"AsyncTCGApi",
|
|
@@ -59,6 +62,9 @@ __all__ = [
|
|
|
59
62
|
"PriceHistoryPoint",
|
|
60
63
|
"BulkCard",
|
|
61
64
|
"BulkPriceRow",
|
|
65
|
+
"ConditionPrice",
|
|
66
|
+
"BulkConditionRow",
|
|
67
|
+
"ConditionMeta",
|
|
62
68
|
"ApiKeySummary",
|
|
63
69
|
"ApiKeyCreated",
|
|
64
70
|
"UsageResponse",
|
|
@@ -137,6 +137,33 @@ class BulkCard(Card):
|
|
|
137
137
|
prices: list[Price] = Field(default_factory=list)
|
|
138
138
|
|
|
139
139
|
|
|
140
|
+
class ConditionPrice(_Base):
|
|
141
|
+
"""Per-condition price floors for one (printing, condition) pair (Pro+)."""
|
|
142
|
+
|
|
143
|
+
card_id: int
|
|
144
|
+
printing: str | None = None
|
|
145
|
+
# TCGPlayer condition grade, e.g. "Near Mint", "Lightly Played".
|
|
146
|
+
condition: str
|
|
147
|
+
language: str | None = None
|
|
148
|
+
# Lowest listed item price for this printing + condition.
|
|
149
|
+
low_price: float | None = None
|
|
150
|
+
# Lowest listed price including shipping.
|
|
151
|
+
lowest_with_shipping: float | None = None
|
|
152
|
+
# Median of sampled shipping-inclusive prices. None when sample_count < 3 —
|
|
153
|
+
# prefer this over low_price when pricing inventory (robust to $0.01 junk listings).
|
|
154
|
+
median_with_shipping: float | None = None
|
|
155
|
+
# Listings sampled for this row (not total market depth — see ConditionMeta.condition_counts).
|
|
156
|
+
sample_count: int | None = None
|
|
157
|
+
last_updated_at: str | None = None
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class BulkConditionRow(ConditionPrice):
|
|
161
|
+
"""Row shape of /bulk/conditions — a ConditionPrice plus card identity."""
|
|
162
|
+
|
|
163
|
+
name: str | None = None
|
|
164
|
+
tcgplayer_id: int | None = None
|
|
165
|
+
|
|
166
|
+
|
|
140
167
|
class PriceHistoryPoint(_Base):
|
|
141
168
|
date: str
|
|
142
169
|
printing: str | None = None
|
|
@@ -153,6 +180,20 @@ class Meta(_Base):
|
|
|
153
180
|
has_more: bool | None = None
|
|
154
181
|
|
|
155
182
|
|
|
183
|
+
class ConditionMeta(Meta):
|
|
184
|
+
"""Meta for /cards/{id}/prices/conditions — cache/staleness signals."""
|
|
185
|
+
|
|
186
|
+
# True when served from cache without a live refresh.
|
|
187
|
+
cached: bool | None = None
|
|
188
|
+
# Present (True) when rows are older than 24h and a live refresh
|
|
189
|
+
# wasn't possible (quota or upstream failure).
|
|
190
|
+
stale: bool | None = None
|
|
191
|
+
as_of: str | None = None
|
|
192
|
+
# Total live listings per condition across ALL printings
|
|
193
|
+
# (only present on live-refreshed responses).
|
|
194
|
+
condition_counts: dict[str, int] | None = None
|
|
195
|
+
|
|
196
|
+
|
|
156
197
|
class RateLimit(_Base):
|
|
157
198
|
daily_limit: int
|
|
158
199
|
daily_remaining: int
|
|
@@ -11,8 +11,14 @@ from ..models import Meta, RateLimit, Response
|
|
|
11
11
|
T = TypeVar("T")
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
def parse_response(
|
|
15
|
-
|
|
14
|
+
def parse_response(
|
|
15
|
+
model: type[T], body: dict[str, Any], *, meta_model: type[Meta] = Meta
|
|
16
|
+
) -> Response[T]:
|
|
17
|
+
"""Coerce a raw response body into a typed Response[T] wrapper.
|
|
18
|
+
|
|
19
|
+
`meta_model` overrides the meta shape for endpoints with a non-standard
|
|
20
|
+
meta (e.g. ConditionMeta for per-condition prices).
|
|
21
|
+
"""
|
|
16
22
|
raw_data = body.get("data")
|
|
17
23
|
if isinstance(model, type) and issubclass(model, BaseModel):
|
|
18
24
|
data = model.model_validate(raw_data)
|
|
@@ -22,6 +28,6 @@ def parse_response(model: type[T], body: dict[str, Any]) -> Response[T]:
|
|
|
22
28
|
|
|
23
29
|
data = TypeAdapter(model).validate_python(raw_data)
|
|
24
30
|
|
|
25
|
-
meta =
|
|
31
|
+
meta = meta_model.model_validate(body["meta"]) if body.get("meta") else None
|
|
26
32
|
rate_limit = RateLimit.model_validate(body["rate_limit"]) if body.get("rate_limit") else None
|
|
27
33
|
return Response[T](data=data, meta=meta, rate_limit=rate_limit) # type: ignore[valid-type]
|
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from typing import TYPE_CHECKING, Literal
|
|
6
6
|
|
|
7
|
-
from ..models import BulkCard, BulkPriceRow, PriceHistoryPoint, Response
|
|
7
|
+
from ..models import BulkCard, BulkConditionRow, BulkPriceRow, PriceHistoryPoint, Response
|
|
8
8
|
from ._base import parse_response
|
|
9
9
|
|
|
10
10
|
if TYPE_CHECKING:
|
|
@@ -14,6 +14,7 @@ if TYPE_CHECKING:
|
|
|
14
14
|
BULK_PRICES_MAX = 500
|
|
15
15
|
BULK_CARDS_MAX = 100
|
|
16
16
|
BULK_HISTORY_MAX = 50
|
|
17
|
+
BULK_CONDITIONS_MAX = 500
|
|
17
18
|
|
|
18
19
|
RangeLiteral = Literal["month", "quarter", "year", "all"]
|
|
19
20
|
|
|
@@ -56,6 +57,24 @@ class BulkResource:
|
|
|
56
57
|
last = resp
|
|
57
58
|
return Response[list[BulkCard]](data=rows, rate_limit=last.rate_limit if last else None)
|
|
58
59
|
|
|
60
|
+
def conditions(self, ids: list[int]) -> Response[list[BulkConditionRow]]:
|
|
61
|
+
# Pro+ only. Cache-only: cards without condition data yet are absent —
|
|
62
|
+
# request them once via cards.conditions() to warm them.
|
|
63
|
+
if len(ids) <= BULK_CONDITIONS_MAX:
|
|
64
|
+
body = self._client._request("GET", "/bulk/conditions", {"ids": _ids(ids)})
|
|
65
|
+
return parse_response(list[BulkConditionRow], body)
|
|
66
|
+
rows: list[BulkConditionRow] = []
|
|
67
|
+
last: Response[list[BulkConditionRow]] | None = None
|
|
68
|
+
for i in range(0, len(ids), BULK_CONDITIONS_MAX):
|
|
69
|
+
chunk = ids[i : i + BULK_CONDITIONS_MAX]
|
|
70
|
+
body = self._client._request("GET", "/bulk/conditions", {"ids": _ids(chunk)})
|
|
71
|
+
resp: Response[list[BulkConditionRow]] = parse_response(list[BulkConditionRow], body)
|
|
72
|
+
rows.extend(resp.data)
|
|
73
|
+
last = resp
|
|
74
|
+
return Response[list[BulkConditionRow]](
|
|
75
|
+
data=rows, rate_limit=last.rate_limit if last else None
|
|
76
|
+
)
|
|
77
|
+
|
|
59
78
|
def history(
|
|
60
79
|
self,
|
|
61
80
|
ids: list[int],
|
|
@@ -114,6 +133,24 @@ class AsyncBulkResource:
|
|
|
114
133
|
last = resp
|
|
115
134
|
return Response[list[BulkCard]](data=rows, rate_limit=last.rate_limit if last else None)
|
|
116
135
|
|
|
136
|
+
async def conditions(self, ids: list[int]) -> Response[list[BulkConditionRow]]:
|
|
137
|
+
# Pro+ only. Cache-only: cards without condition data yet are absent —
|
|
138
|
+
# request them once via cards.conditions() to warm them.
|
|
139
|
+
if len(ids) <= BULK_CONDITIONS_MAX:
|
|
140
|
+
body = await self._client._request("GET", "/bulk/conditions", {"ids": _ids(ids)})
|
|
141
|
+
return parse_response(list[BulkConditionRow], body)
|
|
142
|
+
rows: list[BulkConditionRow] = []
|
|
143
|
+
last: Response[list[BulkConditionRow]] | None = None
|
|
144
|
+
for i in range(0, len(ids), BULK_CONDITIONS_MAX):
|
|
145
|
+
chunk = ids[i : i + BULK_CONDITIONS_MAX]
|
|
146
|
+
body = await self._client._request("GET", "/bulk/conditions", {"ids": _ids(chunk)})
|
|
147
|
+
resp: Response[list[BulkConditionRow]] = parse_response(list[BulkConditionRow], body)
|
|
148
|
+
rows.extend(resp.data)
|
|
149
|
+
last = resp
|
|
150
|
+
return Response[list[BulkConditionRow]](
|
|
151
|
+
data=rows, rate_limit=last.rate_limit if last else None
|
|
152
|
+
)
|
|
153
|
+
|
|
117
154
|
async def history(
|
|
118
155
|
self,
|
|
119
156
|
ids: list[int],
|
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from typing import TYPE_CHECKING, Literal
|
|
6
6
|
|
|
7
|
-
from ..models import BulkCard, Card, Price, PriceHistoryPoint, Response
|
|
7
|
+
from ..models import BulkCard, Card, ConditionMeta, ConditionPrice, Price, PriceHistoryPoint, Response
|
|
8
8
|
from ._base import parse_response
|
|
9
9
|
|
|
10
10
|
if TYPE_CHECKING:
|
|
@@ -26,6 +26,15 @@ class CardsResource:
|
|
|
26
26
|
body = self._client._request("GET", f"/cards/{card_id}/prices", {"printing": printing})
|
|
27
27
|
return parse_response(list[Price], body)
|
|
28
28
|
|
|
29
|
+
def conditions(
|
|
30
|
+
self, card_id: int, *, printing: str | None = None
|
|
31
|
+
) -> Response[list[ConditionPrice]]:
|
|
32
|
+
# Pro+ only. `meta` is a ConditionMeta (cached/stale/as_of/condition_counts).
|
|
33
|
+
body = self._client._request(
|
|
34
|
+
"GET", f"/cards/{card_id}/prices/conditions", {"printing": printing}
|
|
35
|
+
)
|
|
36
|
+
return parse_response(list[ConditionPrice], body, meta_model=ConditionMeta)
|
|
37
|
+
|
|
29
38
|
def by_tcgplayer_id(self, tcgplayer_id: int) -> Response[BulkCard]:
|
|
30
39
|
body = self._client._request("GET", f"/cards/tcgplayer/{tcgplayer_id}")
|
|
31
40
|
return parse_response(BulkCard, body)
|
|
@@ -63,6 +72,15 @@ class AsyncCardsResource:
|
|
|
63
72
|
body = await self._client._request("GET", f"/cards/{card_id}/prices", {"printing": printing})
|
|
64
73
|
return parse_response(list[Price], body)
|
|
65
74
|
|
|
75
|
+
async def conditions(
|
|
76
|
+
self, card_id: int, *, printing: str | None = None
|
|
77
|
+
) -> Response[list[ConditionPrice]]:
|
|
78
|
+
# Pro+ only. `meta` is a ConditionMeta (cached/stale/as_of/condition_counts).
|
|
79
|
+
body = await self._client._request(
|
|
80
|
+
"GET", f"/cards/{card_id}/prices/conditions", {"printing": printing}
|
|
81
|
+
)
|
|
82
|
+
return parse_response(list[ConditionPrice], body, meta_model=ConditionMeta)
|
|
83
|
+
|
|
66
84
|
async def by_tcgplayer_id(self, tcgplayer_id: int) -> Response[BulkCard]:
|
|
67
85
|
body = await self._client._request("GET", f"/cards/tcgplayer/{tcgplayer_id}")
|
|
68
86
|
return parse_response(BulkCard, body)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|