tradera-cli 0.1.1__tar.gz → 0.1.2__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.
Files changed (22) hide show
  1. {tradera_cli-0.1.1 → tradera_cli-0.1.2}/PKG-INFO +1 -1
  2. {tradera_cli-0.1.1 → tradera_cli-0.1.2}/pyproject.toml +1 -1
  3. {tradera_cli-0.1.1 → tradera_cli-0.1.2}/tests/test_api.py +45 -0
  4. {tradera_cli-0.1.1 → tradera_cli-0.1.2}/tests/test_cli.py +78 -1
  5. {tradera_cli-0.1.1 → tradera_cli-0.1.2}/tradera_cli/api.py +43 -4
  6. {tradera_cli-0.1.1 → tradera_cli-0.1.2}/tradera_cli/cli.py +67 -4
  7. {tradera_cli-0.1.1 → tradera_cli-0.1.2}/tradera_cli.egg-info/PKG-INFO +1 -1
  8. {tradera_cli-0.1.1 → tradera_cli-0.1.2}/LICENSE +0 -0
  9. {tradera_cli-0.1.1 → tradera_cli-0.1.2}/README.md +0 -0
  10. {tradera_cli-0.1.1 → tradera_cli-0.1.2}/setup.cfg +0 -0
  11. {tradera_cli-0.1.1 → tradera_cli-0.1.2}/tests/test_cli_entry.py +0 -0
  12. {tradera_cli-0.1.1 → tradera_cli-0.1.2}/tests/test_formatters.py +0 -0
  13. {tradera_cli-0.1.1 → tradera_cli-0.1.2}/tests/test_import.py +0 -0
  14. {tradera_cli-0.1.1 → tradera_cli-0.1.2}/tests/test_main_module.py +0 -0
  15. {tradera_cli-0.1.1 → tradera_cli-0.1.2}/tradera_cli/__init__.py +0 -0
  16. {tradera_cli-0.1.1 → tradera_cli-0.1.2}/tradera_cli/__main__.py +0 -0
  17. {tradera_cli-0.1.1 → tradera_cli-0.1.2}/tradera_cli/formatters.py +0 -0
  18. {tradera_cli-0.1.1 → tradera_cli-0.1.2}/tradera_cli.egg-info/SOURCES.txt +0 -0
  19. {tradera_cli-0.1.1 → tradera_cli-0.1.2}/tradera_cli.egg-info/dependency_links.txt +0 -0
  20. {tradera_cli-0.1.1 → tradera_cli-0.1.2}/tradera_cli.egg-info/entry_points.txt +0 -0
  21. {tradera_cli-0.1.1 → tradera_cli-0.1.2}/tradera_cli.egg-info/requires.txt +0 -0
  22. {tradera_cli-0.1.1 → tradera_cli-0.1.2}/tradera_cli.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tradera-cli
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: CLI for Tradera web endpoints
5
5
  Author: Johnny W
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tradera-cli"
7
- version = "0.1.1"
7
+ version = "0.1.2"
8
8
  description = "CLI for Tradera web endpoints"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -420,6 +420,45 @@ def test_item_returns_dict_or_raises() -> None:
420
420
  client.item(123)
421
421
 
422
422
 
423
+ def test_item_payment_calculations_returns_dict() -> None:
424
+ client = TraderaClient()
425
+ next_data = {
426
+ "props": {
427
+ "pageProps": {
428
+ "initialState": {
429
+ "views": {
430
+ "viewItem": {
431
+ "itemDetails": {
432
+ "paymentCalculations": {
433
+ "paymentAmountForBid": 4927,
434
+ "buyerProtectionCostForFixedPrice": 242,
435
+ }
436
+ }
437
+ }
438
+ }
439
+ }
440
+ }
441
+ }
442
+ }
443
+ client._request = lambda *_args, **_kwargs: ( # type: ignore[method-assign]
444
+ '<script id="__NEXT_DATA__" type="application/json">'
445
+ f"{json.dumps(next_data)}"
446
+ "</script>"
447
+ )
448
+
449
+ result = client.item_payment_calculations(123)
450
+ assert result["paymentAmountForBid"] == 4927
451
+ assert result["buyerProtectionCostForFixedPrice"] == 242
452
+
453
+
454
+ def test_item_payment_calculations_raises_on_missing_next_data() -> None:
455
+ client = TraderaClient()
456
+ client._request = lambda *_args, **_kwargs: "<html></html>" # type: ignore[method-assign]
457
+
458
+ with pytest.raises(TraderaApiError, match="Could not parse item page response"):
459
+ client.item_payment_calculations(123)
460
+
461
+
423
462
  def test_categories_calls_expected_path() -> None:
424
463
  client = TraderaClient()
425
464
  captured: dict[str, str] = {}
@@ -440,5 +479,11 @@ def test_categories_calls_expected_path() -> None:
440
479
  def test_parse_item_id_from_digits_and_url_and_invalid() -> None:
441
480
  assert parse_item_id("123456") == 123456
442
481
  assert parse_item_id("https://www.tradera.com/item/123456789/test") == 123456789
482
+ assert (
483
+ parse_item_id(
484
+ "https://www.tradera.com/item/1000006/707858418/playstation-5-disc-edition-the-last-of-us-dualsense"
485
+ )
486
+ == 707858418
487
+ )
443
488
  with pytest.raises(ValueError, match="Could not parse item id"):
444
489
  parse_item_id("https://www.tradera.com/item/no-id")
@@ -23,6 +23,11 @@ class DummyClient:
23
23
  raise self.error
24
24
  return self.payload or {}
25
25
 
26
+ def item_payment_calculations(self, _item_id: int) -> dict:
27
+ if self.error:
28
+ raise self.error
29
+ return {}
30
+
26
31
  def categories(self, **_kwargs: object) -> list:
27
32
  if self.error:
28
33
  raise self.error
@@ -92,7 +97,7 @@ def test_cmd_search_and_no_translate(monkeypatch: pytest.MonkeyPatch, capsys: py
92
97
  assert "itemId" in out
93
98
 
94
99
 
95
- def test_cmd_item_table_and_json(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None:
100
+ def test_cmd_item_table_json_and_jsonl(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None:
96
101
  class ItemClient(DummyClient):
97
102
  def item(self, _item_id: int) -> dict:
98
103
  return {
@@ -100,9 +105,13 @@ def test_cmd_item_table_and_json(monkeypatch: pytest.MonkeyPatch, capsys: pytest
100
105
  "shortDescription": "Demo",
101
106
  "buyNowPrice": 99,
102
107
  "currency": "SEK",
108
+ "buyerProtectionInfo": {"totalPrice": 109, "fee": 10},
103
109
  "seller": {"alias": "john"},
104
110
  }
105
111
 
112
+ def item_payment_calculations(self, _item_id: int) -> dict:
113
+ return {}
114
+
106
115
  monkeypatch.setattr(cli, "TraderaClient", ItemClient)
107
116
 
108
117
  table_args = argparse.Namespace(item="12", format="table")
@@ -110,11 +119,24 @@ def test_cmd_item_table_and_json(monkeypatch: pytest.MonkeyPatch, capsys: pytest
110
119
  table_out = capsys.readouterr().out
111
120
  assert "seller" in table_out
112
121
  assert "john" in table_out
122
+ assert "buyerProtectionTotal" in table_out
123
+ assert "109" in table_out
124
+ assert "10" in table_out
113
125
 
114
126
  json_args = argparse.Namespace(item="12", format="json")
115
127
  assert cli.cmd_item(json_args) == 0
116
128
  json_out = capsys.readouterr().out
117
129
  assert '"itemId": 12' in json_out
130
+ assert '"buyerProtectionTotal": 109' in json_out
131
+ assert '"buyerProtectionFee": 10' in json_out
132
+
133
+ jsonl_args = argparse.Namespace(item="12", format="jsonl")
134
+ assert cli.cmd_item(jsonl_args) == 0
135
+ jsonl_out = capsys.readouterr().out.strip()
136
+ assert '"itemId": 12' in jsonl_out
137
+ assert '"buyerProtectionTotal": 109' in jsonl_out
138
+ assert '"buyerProtectionFee": 10' in jsonl_out
139
+ assert "\n" not in jsonl_out
118
140
 
119
141
 
120
142
  def test_cmd_item_table_handles_non_dict_seller(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None:
@@ -122,6 +144,9 @@ def test_cmd_item_table_handles_non_dict_seller(monkeypatch: pytest.MonkeyPatch,
122
144
  def item(self, _item_id: int) -> dict:
123
145
  return {"id": 33, "title": "No seller dict", "seller": "none"}
124
146
 
147
+ def item_payment_calculations(self, _item_id: int) -> dict:
148
+ return {}
149
+
125
150
  monkeypatch.setattr(cli, "TraderaClient", ItemClient)
126
151
 
127
152
  args = argparse.Namespace(item="33", format="table")
@@ -130,6 +155,58 @@ def test_cmd_item_table_handles_non_dict_seller(monkeypatch: pytest.MonkeyPatch,
130
155
  assert "No seller dict" in out
131
156
 
132
157
 
158
+ def test_cmd_item_table_supports_hyphenated_buyer_protection_key(
159
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
160
+ ) -> None:
161
+ class ItemClient(DummyClient):
162
+ def item(self, _item_id: int) -> dict:
163
+ return {
164
+ "id": 44,
165
+ "title": "With fee",
166
+ "buyer-protection-info": {"priceTotal": 220, "serviceFee": 20},
167
+ }
168
+
169
+ def item_payment_calculations(self, _item_id: int) -> dict:
170
+ return {}
171
+
172
+ monkeypatch.setattr(cli, "TraderaClient", ItemClient)
173
+
174
+ args = argparse.Namespace(item="44", format="table")
175
+ assert cli.cmd_item(args) == 0
176
+ out = capsys.readouterr().out
177
+ assert "buyerProtectionTotal" in out
178
+ assert "220" in out
179
+ assert "20" in out
180
+
181
+
182
+ def test_cmd_item_table_uses_payment_calculations_for_bid_total(
183
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
184
+ ) -> None:
185
+ class ItemClient(DummyClient):
186
+ def item(self, _item_id: int) -> dict:
187
+ return {
188
+ "itemId": 55,
189
+ "shortDescription": "Auction item",
190
+ "leadingBid": 4690,
191
+ "bidCount": 1,
192
+ "currency": "SEK",
193
+ "seller": {"alias": "john"},
194
+ }
195
+
196
+ def item_payment_calculations(self, _item_id: int) -> dict:
197
+ return {"paymentAmountForBid": 4927}
198
+
199
+ monkeypatch.setattr(cli, "TraderaClient", ItemClient)
200
+
201
+ args = argparse.Namespace(item="55", format="table")
202
+ assert cli.cmd_item(args) == 0
203
+ out = capsys.readouterr().out
204
+ assert "buyerProtectionTotal" in out
205
+ assert "4927" in out
206
+ assert "237" in out
207
+ assert "4690" in out
208
+
209
+
133
210
  def test_cmd_categories_jsonl(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None:
134
211
  class CategoriesClient(DummyClient):
135
212
  def categories(self, **_kwargs: object) -> list:
@@ -4,7 +4,7 @@ import json
4
4
  import re
5
5
  from dataclasses import dataclass
6
6
  from typing import Any
7
- from urllib.parse import urlencode
7
+ from urllib.parse import urlencode, urlparse
8
8
 
9
9
  import requests
10
10
  from requests import RequestException
@@ -200,6 +200,33 @@ class TraderaClient:
200
200
  raise TraderaApiError(f"Unexpected item response for item {item_id}")
201
201
  return data
202
202
 
203
+ def item_payment_calculations(self, item_id: int) -> dict[str, Any]:
204
+ html = self._request("GET", f"/item/{item_id}")
205
+ if not isinstance(html, str):
206
+ raise TraderaApiError(f"Unexpected item page response for item {item_id}")
207
+
208
+ match = re.search(r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>', html)
209
+ if not match:
210
+ raise TraderaApiError(f"Could not parse item page response for item {item_id}")
211
+
212
+ try:
213
+ data = json.loads(match.group(1))
214
+ except ValueError as exc:
215
+ raise TraderaApiError(f"Invalid item page data for item {item_id}") from exc
216
+
217
+ payment_calculations = (
218
+ data.get("props", {})
219
+ .get("pageProps", {})
220
+ .get("initialState", {})
221
+ .get("views", {})
222
+ .get("viewItem", {})
223
+ .get("itemDetails", {})
224
+ .get("paymentCalculations", {})
225
+ )
226
+ if not isinstance(payment_calculations, dict):
227
+ raise TraderaApiError(f"Unexpected payment calculations for item {item_id}")
228
+ return payment_calculations
229
+
203
230
  def categories(self, level: int = 1, lang: str = "sv") -> Any:
204
231
  return self._request("GET", f"/api/categories/{level}?languageCodeIso2={lang}&next=1")
205
232
 
@@ -207,7 +234,19 @@ class TraderaClient:
207
234
  def parse_item_id(value: str) -> int:
208
235
  if value.isdigit():
209
236
  return int(value)
210
- match = re.search(r"/(\d{6,})", value)
211
- if not match:
237
+ parsed = urlparse(value)
238
+ path = parsed.path or value
239
+ segments = [segment for segment in path.split("/") if segment]
240
+
241
+ if "item" in segments:
242
+ item_index = segments.index("item")
243
+ numeric_after_item = [segment for segment in segments[item_index + 1 :] if segment.isdigit()]
244
+ if len(numeric_after_item) >= 2:
245
+ return int(numeric_after_item[1])
246
+ if numeric_after_item:
247
+ return int(numeric_after_item[0])
248
+
249
+ matches = re.findall(r"(\d{6,})", value)
250
+ if not matches:
212
251
  raise ValueError(f"Could not parse item id from: {value}")
213
- return int(match.group(1))
252
+ return int(matches[-1])
@@ -14,6 +14,32 @@ from .formatters import (
14
14
  )
15
15
 
16
16
 
17
+ def _extract_buyer_protection(data: dict[str, Any], payment_calculations: dict[str, Any] | None = None) -> tuple[Any, Any]:
18
+ info = data.get("buyerProtectionInfo")
19
+ if not isinstance(info, dict):
20
+ info = data.get("buyer-protection-info")
21
+ if not isinstance(info, dict):
22
+ info = {}
23
+
24
+ total = info.get("total") or info.get("totalPrice") or info.get("totalAmount") or info.get("priceTotal") or ""
25
+ fee = info.get("fee") or info.get("buyerProtectionFee") or info.get("serviceFee") or ""
26
+
27
+ if isinstance(payment_calculations, dict):
28
+ if data.get("bidCount") and payment_calculations.get("paymentAmountForBid") is not None:
29
+ total = payment_calculations.get("paymentAmountForBid")
30
+ leading_bid = data.get("leadingBid")
31
+ if isinstance(leading_bid, (int, float)) and isinstance(total, (int, float)):
32
+ fee = total - leading_bid
33
+ elif payment_calculations.get("paymentAmountForFixedPrice") is not None:
34
+ total = payment_calculations.get("paymentAmountForFixedPrice")
35
+ fixed_price = data.get("fixedPrice")
36
+ if payment_calculations.get("buyerProtectionCostForFixedPrice") is not None:
37
+ fee = payment_calculations.get("buyerProtectionCostForFixedPrice")
38
+ elif isinstance(fixed_price, (int, float)) and isinstance(total, (int, float)):
39
+ fee = total - fixed_price
40
+ return total, fee
41
+
42
+
17
43
  def _print_output(raw: Any, rows: list[dict[str, Any]], fmt: str, columns: list[str]) -> None:
18
44
  if fmt == "json":
19
45
  print(to_json(raw))
@@ -57,17 +83,54 @@ def cmd_item(args: argparse.Namespace) -> int:
57
83
  client = TraderaClient()
58
84
  item_id = parse_item_id(args.item)
59
85
  data = client.item(item_id)
86
+ payment_calculations: dict[str, Any] = {}
87
+ try:
88
+ payment_calculations = client.item_payment_calculations(item_id)
89
+ except TraderaApiError:
90
+ payment_calculations = {}
91
+
92
+ buyer_protection_total, buyer_protection_fee = _extract_buyer_protection(data, payment_calculations)
93
+
60
94
  if args.format == "table":
61
95
  row = {
62
96
  "itemId": data.get("itemId") or data.get("id") or item_id,
63
97
  "title": data.get("shortDescription") or data.get("title") or "",
64
- "price": data.get("buyNowPrice") or data.get("nextBid") or data.get("price") or "",
98
+ "price": (
99
+ data.get("buyNowPrice")
100
+ or data.get("nextBid")
101
+ or data.get("price")
102
+ or data.get("leadingBid")
103
+ or data.get("fixedPrice")
104
+ or data.get("openingBid")
105
+ or ""
106
+ ),
65
107
  "currency": data.get("currency") or "SEK",
108
+ "buyerProtectionTotal": buyer_protection_total,
109
+ "buyerProtectionFee": buyer_protection_fee,
66
110
  "seller": (data.get("seller") or {}).get("alias") if isinstance(data.get("seller"), dict) else "",
67
111
  }
68
- print(to_table([row], ["itemId", "title", "price", "currency", "seller"]))
112
+ print(
113
+ to_table(
114
+ [row],
115
+ ["itemId", "title", "price", "currency", "buyerProtectionTotal", "buyerProtectionFee", "seller"],
116
+ )
117
+ )
69
118
  else:
70
- print(to_json(data))
119
+ enriched: dict[str, Any] = {}
120
+ inserted = False
121
+ for key, value in data.items():
122
+ enriched[key] = value
123
+ if key == "leadingBid":
124
+ enriched["buyerProtectionTotal"] = buyer_protection_total
125
+ enriched["buyerProtectionFee"] = buyer_protection_fee
126
+ inserted = True
127
+ if not inserted:
128
+ enriched["buyerProtectionTotal"] = buyer_protection_total
129
+ enriched["buyerProtectionFee"] = buyer_protection_fee
130
+ if args.format == "jsonl":
131
+ print(to_jsonl([enriched]))
132
+ else:
133
+ print(to_json(enriched))
71
134
  return 0
72
135
 
73
136
 
@@ -104,7 +167,7 @@ def build_parser() -> argparse.ArgumentParser:
104
167
 
105
168
  item = subparsers.add_parser("item", help="Get item details")
106
169
  item.add_argument("item", help="Item id or item URL")
107
- item.add_argument("--format", choices=["table", "json"], default="json")
170
+ item.add_argument("--format", choices=["table", "json", "jsonl"], default="json")
108
171
  item.set_defaults(func=cmd_item)
109
172
 
110
173
  categories = subparsers.add_parser("categories", help="List categories by level")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tradera-cli
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: CLI for Tradera web endpoints
5
5
  Author: Johnny W
6
6
  License-Expression: MIT
File without changes
File without changes
File without changes