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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tcgapi
3
- Version: 0.2.1
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.2.1"
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.2.1"
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(model: type[T], body: dict[str, Any]) -> Response[T]:
15
- """Coerce a raw response body into a typed Response[T] wrapper."""
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 = Meta.model_validate(body["meta"]) if body.get("meta") else None
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