tradera-cli 0.1.4__tar.gz → 0.1.6__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.
- {tradera_cli-0.1.4/tradera_cli.egg-info → tradera_cli-0.1.6}/PKG-INFO +1 -1
- {tradera_cli-0.1.4 → tradera_cli-0.1.6}/pyproject.toml +1 -1
- {tradera_cli-0.1.4 → tradera_cli-0.1.6}/tests/test_api.py +3 -1
- {tradera_cli-0.1.4 → tradera_cli-0.1.6}/tradera_cli/api.py +87 -3
- {tradera_cli-0.1.4 → tradera_cli-0.1.6/tradera_cli.egg-info}/PKG-INFO +1 -1
- {tradera_cli-0.1.4 → tradera_cli-0.1.6}/LICENSE +0 -0
- {tradera_cli-0.1.4 → tradera_cli-0.1.6}/README.md +0 -0
- {tradera_cli-0.1.4 → tradera_cli-0.1.6}/setup.cfg +0 -0
- {tradera_cli-0.1.4 → tradera_cli-0.1.6}/tests/test_cli.py +0 -0
- {tradera_cli-0.1.4 → tradera_cli-0.1.6}/tests/test_cli_entry.py +0 -0
- {tradera_cli-0.1.4 → tradera_cli-0.1.6}/tests/test_formatters.py +0 -0
- {tradera_cli-0.1.4 → tradera_cli-0.1.6}/tests/test_import.py +0 -0
- {tradera_cli-0.1.4 → tradera_cli-0.1.6}/tests/test_main_module.py +0 -0
- {tradera_cli-0.1.4 → tradera_cli-0.1.6}/tradera_cli/__init__.py +0 -0
- {tradera_cli-0.1.4 → tradera_cli-0.1.6}/tradera_cli/__main__.py +0 -0
- {tradera_cli-0.1.4 → tradera_cli-0.1.6}/tradera_cli/cli.py +0 -0
- {tradera_cli-0.1.4 → tradera_cli-0.1.6}/tradera_cli/formatters.py +0 -0
- {tradera_cli-0.1.4 → tradera_cli-0.1.6}/tradera_cli.egg-info/SOURCES.txt +0 -0
- {tradera_cli-0.1.4 → tradera_cli-0.1.6}/tradera_cli.egg-info/dependency_links.txt +0 -0
- {tradera_cli-0.1.4 → tradera_cli-0.1.6}/tradera_cli.egg-info/entry_points.txt +0 -0
- {tradera_cli-0.1.4 → tradera_cli-0.1.6}/tradera_cli.egg-info/requires.txt +0 -0
- {tradera_cli-0.1.4 → tradera_cli-0.1.6}/tradera_cli.egg-info/top_level.txt +0 -0
|
@@ -425,12 +425,14 @@ def test_search_uses_search_page_for_search_type() -> None:
|
|
|
425
425
|
|
|
426
426
|
def test_search_page_raises_when_next_data_is_missing() -> None:
|
|
427
427
|
client = TraderaClient()
|
|
428
|
-
|
|
428
|
+
# Use a response that has neither __NEXT_DATA__ nor any item links
|
|
429
|
+
client._request = lambda *_args, **_kwargs: "<html><body>No data here</body></html>" # type: ignore[method-assign]
|
|
429
430
|
|
|
430
431
|
with pytest.raises(TraderaApiError, match="Could not parse search page response"):
|
|
431
432
|
client.search("kamera", item_status="Unsold")
|
|
432
433
|
|
|
433
434
|
|
|
435
|
+
|
|
434
436
|
def test_item_returns_dict_or_raises() -> None:
|
|
435
437
|
client = TraderaClient()
|
|
436
438
|
next_data = {
|
|
@@ -69,7 +69,7 @@ class TraderaClient:
|
|
|
69
69
|
return response.text
|
|
70
70
|
|
|
71
71
|
def _next_data_from_html(self, html: str, context: str) -> dict[str, Any]:
|
|
72
|
-
match = re.search(r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>', html)
|
|
72
|
+
match = re.search(r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>', html, re.DOTALL)
|
|
73
73
|
if not match:
|
|
74
74
|
raise TraderaApiError(f"Could not parse {context}")
|
|
75
75
|
|
|
@@ -82,6 +82,44 @@ class TraderaClient:
|
|
|
82
82
|
raise TraderaApiError(f"Unexpected {context}")
|
|
83
83
|
return data
|
|
84
84
|
|
|
85
|
+
def _extract_json_ld_product(self, html: str) -> dict[str, Any] | None:
|
|
86
|
+
# Find all JSON-LD script blocks
|
|
87
|
+
scripts = re.findall(r'<script type="application/ld\+json">(.*?)</script>', html, re.DOTALL)
|
|
88
|
+
for script_content in scripts:
|
|
89
|
+
try:
|
|
90
|
+
data = json.loads(script_content)
|
|
91
|
+
# JSON-LD can be a list of objects or a single object
|
|
92
|
+
items = data if isinstance(data, list) else [data]
|
|
93
|
+
for item in items:
|
|
94
|
+
if isinstance(item, dict) and item.get("@type") == "Product":
|
|
95
|
+
offers = item.get("offers")
|
|
96
|
+
offer_data = offers if isinstance(offers, dict) else (offers[0] if isinstance(offers, list) and offers else {})
|
|
97
|
+
|
|
98
|
+
availability = offer_data.get("availability", "")
|
|
99
|
+
status = "Unknown"
|
|
100
|
+
if "InStock" in availability:
|
|
101
|
+
status = "Ongoing"
|
|
102
|
+
elif "OutOfStock" in availability:
|
|
103
|
+
status = "Ended"
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
"itemId": item.get("sku"),
|
|
107
|
+
"title": item.get("name"),
|
|
108
|
+
"shortDescription": item.get("name"),
|
|
109
|
+
"description": item.get("description"),
|
|
110
|
+
"price": offer_data.get("price"),
|
|
111
|
+
"buyNowPrice": offer_data.get("price"),
|
|
112
|
+
"fixedPrice": offer_data.get("price"),
|
|
113
|
+
"leadingBid": offer_data.get("price"),
|
|
114
|
+
"openingBid": offer_data.get("price"),
|
|
115
|
+
"currency": offer_data.get("priceCurrency", "SEK"),
|
|
116
|
+
"mainImage": item.get("image"),
|
|
117
|
+
"status": status,
|
|
118
|
+
}
|
|
119
|
+
except (ValueError, TypeError):
|
|
120
|
+
continue
|
|
121
|
+
return None
|
|
122
|
+
|
|
85
123
|
def _ensure_client_token(self, force: bool = False) -> None:
|
|
86
124
|
if not force and self.session.cookies.get("trd_at"):
|
|
87
125
|
return
|
|
@@ -163,7 +201,32 @@ class TraderaClient:
|
|
|
163
201
|
if not isinstance(html, str):
|
|
164
202
|
raise TraderaApiError("Unexpected search page response")
|
|
165
203
|
|
|
166
|
-
|
|
204
|
+
try:
|
|
205
|
+
data = self._next_data_from_html(html, "search page response")
|
|
206
|
+
except TraderaApiError:
|
|
207
|
+
# Fallback to scraping the HTML for search results
|
|
208
|
+
items = []
|
|
209
|
+
# Find all item links: /item/{catId}/{itemId}/{slug}
|
|
210
|
+
links = re.findall(r'href="/item/([0-9]+)/([0-9]+)/([^"]*)"', html)
|
|
211
|
+
|
|
212
|
+
if not links:
|
|
213
|
+
raise TraderaApiError("Could not parse search page response")
|
|
214
|
+
|
|
215
|
+
# To avoid duplicates
|
|
216
|
+
seen_ids = set()
|
|
217
|
+
for cat_id, item_id, slug in links:
|
|
218
|
+
if item_id not in seen_ids:
|
|
219
|
+
items.append({
|
|
220
|
+
"itemId": item_id,
|
|
221
|
+
"title": slug.replace("-", " ").replace("_", " ").capitalize(),
|
|
222
|
+
"url": f"/item/{cat_id}/{item_id}/{slug}",
|
|
223
|
+
"price": "Unknown",
|
|
224
|
+
"currency": "SEK",
|
|
225
|
+
"endDate": None,
|
|
226
|
+
})
|
|
227
|
+
seen_ids.add(item_id)
|
|
228
|
+
|
|
229
|
+
return {"items": items}
|
|
167
230
|
|
|
168
231
|
discover = data.get("props", {}).get("pageProps", {}).get("initialState", {}).get("discover")
|
|
169
232
|
if not isinstance(discover, dict):
|
|
@@ -184,7 +247,28 @@ class TraderaClient:
|
|
|
184
247
|
if not isinstance(html, str):
|
|
185
248
|
raise TraderaApiError(f"Unexpected item page response for item {item_id}")
|
|
186
249
|
|
|
187
|
-
|
|
250
|
+
try:
|
|
251
|
+
data = self._next_data_from_html(html, f"item page response for item {item_id}")
|
|
252
|
+
except TraderaApiError:
|
|
253
|
+
# Fallback to JSON-LD extraction
|
|
254
|
+
json_ld_data = self._extract_json_ld_product(html)
|
|
255
|
+
if json_ld_data:
|
|
256
|
+
# Wrap in the structure expected by _item_from_page
|
|
257
|
+
data = {
|
|
258
|
+
"props": {
|
|
259
|
+
"pageProps": {
|
|
260
|
+
"initialState": {
|
|
261
|
+
"views": {
|
|
262
|
+
"viewItem": {
|
|
263
|
+
"itemDetails": json_ld_data
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
else:
|
|
271
|
+
raise
|
|
188
272
|
|
|
189
273
|
view_item = (
|
|
190
274
|
data.get("props", {})
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|