walla-cli 0.1.8__tar.gz → 0.1.10__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 (112) hide show
  1. {walla_cli-0.1.8 → walla_cli-0.1.10}/PKG-INFO +14 -5
  2. {walla_cli-0.1.8 → walla_cli-0.1.10}/README.md +13 -4
  3. {walla_cli-0.1.8 → walla_cli-0.1.10}/docs/WIRE.md +4 -1
  4. walla_cli-0.1.10/docs/assets/walla-demo-poster.jpg +0 -0
  5. walla_cli-0.1.10/docs/assets/walla-demo-preview.gif +0 -0
  6. walla_cli-0.1.10/docs/assets/walla-demo.mp4 +0 -0
  7. walla_cli-0.1.10/fixtures/categories.json +59 -0
  8. {walla_cli-0.1.8 → walla_cli-0.1.10}/pyproject.toml +1 -1
  9. {walla_cli-0.1.8 → walla_cli-0.1.10}/skills/walla-sell/SKILL.md +5 -4
  10. walla_cli-0.1.10/tests/test_categories_find.py +58 -0
  11. {walla_cli-0.1.8 → walla_cli-0.1.10}/tests/test_parsers.py +2 -0
  12. {walla_cli-0.1.8 → walla_cli-0.1.10}/tests/test_sell.py +28 -0
  13. walla_cli-0.1.10/uv.lock +2644 -0
  14. walla_cli-0.1.10/walla/__init__.py +7 -0
  15. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/account/sell.py +31 -3
  16. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/account/sell_wire.py +5 -2
  17. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/cli/banner.py +12 -8
  18. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/cli/cmd_look.py +27 -1
  19. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/cli/cmd_sell.py +36 -10
  20. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/cli/main.py +2 -3
  21. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/core/instruct.py +2 -1
  22. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/mcp/server.py +26 -4
  23. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/models/profile.py +1 -0
  24. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/search/__init__.py +12 -0
  25. walla_cli-0.1.10/walla/search/category_find.py +98 -0
  26. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/search/parse.py +17 -13
  27. walla_cli-0.1.10/walla/search/pick_category.py +47 -0
  28. walla_cli-0.1.8/fixtures/categories.json +0 -49
  29. walla_cli-0.1.8/walla/__init__.py +0 -7
  30. {walla_cli-0.1.8 → walla_cli-0.1.10}/.cursor/rules/quality.mdc +0 -0
  31. {walla_cli-0.1.8 → walla_cli-0.1.10}/.github/workflows/ci.yml +0 -0
  32. {walla_cli-0.1.8 → walla_cli-0.1.10}/.gitignore +0 -0
  33. {walla_cli-0.1.8 → walla_cli-0.1.10}/AGENTS.md +0 -0
  34. {walla_cli-0.1.8 → walla_cli-0.1.10}/LICENSE +0 -0
  35. {walla_cli-0.1.8 → walla_cli-0.1.10}/docs/assets/walla-github-banner.jpg +0 -0
  36. {walla_cli-0.1.8 → walla_cli-0.1.10}/docs/code_quality.md +0 -0
  37. {walla_cli-0.1.8 → walla_cli-0.1.10}/docs/data_engineering_standards.md +0 -0
  38. {walla_cli-0.1.8 → walla_cli-0.1.10}/docs/mcp.md +0 -0
  39. {walla_cli-0.1.8 → walla_cli-0.1.10}/docs/objective_function.md +0 -0
  40. {walla_cli-0.1.8 → walla_cli-0.1.10}/docs/rate_limit_analysis.md +0 -0
  41. {walla_cli-0.1.8 → walla_cli-0.1.10}/fixtures/item_detail.json +0 -0
  42. {walla_cli-0.1.8 → walla_cli-0.1.10}/fixtures/offer_buyer_request.json +0 -0
  43. {walla_cli-0.1.8 → walla_cli-0.1.10}/fixtures/search_bicicleta.json +0 -0
  44. {walla_cli-0.1.8 → walla_cli-0.1.10}/fixtures/sell_item_request.json +0 -0
  45. {walla_cli-0.1.8 → walla_cli-0.1.10}/fixtures/wire_contracts.json +0 -0
  46. {walla_cli-0.1.8 → walla_cli-0.1.10}/scripts/check_code_quality.py +0 -0
  47. {walla_cli-0.1.8 → walla_cli-0.1.10}/scripts/dogfood_run.py +0 -0
  48. {walla_cli-0.1.8 → walla_cli-0.1.10}/scripts/probe_rate.py +0 -0
  49. {walla_cli-0.1.8 → walla_cli-0.1.10}/skills/walla/SKILL.md +0 -0
  50. {walla_cli-0.1.8 → walla_cli-0.1.10}/skills/walla-buy/SKILL.md +0 -0
  51. {walla_cli-0.1.8 → walla_cli-0.1.10}/tests/test_account.py +0 -0
  52. {walla_cli-0.1.8 → walla_cli-0.1.10}/tests/test_account_more.py +0 -0
  53. {walla_cli-0.1.8 → walla_cli-0.1.10}/tests/test_catch.py +0 -0
  54. {walla_cli-0.1.8 → walla_cli-0.1.10}/tests/test_chat_open.py +0 -0
  55. {walla_cli-0.1.8 → walla_cli-0.1.10}/tests/test_cli_json.py +0 -0
  56. {walla_cli-0.1.8 → walla_cli-0.1.10}/tests/test_core.py +0 -0
  57. {walla_cli-0.1.8 → walla_cli-0.1.10}/tests/test_desk.py +0 -0
  58. {walla_cli-0.1.8 → walla_cli-0.1.10}/tests/test_edges.py +0 -0
  59. {walla_cli-0.1.8 → walla_cli-0.1.10}/tests/test_export.py +0 -0
  60. {walla_cli-0.1.8 → walla_cli-0.1.10}/tests/test_http_more.py +0 -0
  61. {walla_cli-0.1.8 → walla_cli-0.1.10}/tests/test_live.py +0 -0
  62. {walla_cli-0.1.8 → walla_cli-0.1.10}/tests/test_negotiate.py +0 -0
  63. {walla_cli-0.1.8 → walla_cli-0.1.10}/tests/test_polite.py +0 -0
  64. {walla_cli-0.1.8 → walla_cli-0.1.10}/tests/test_regressions.py +0 -0
  65. {walla_cli-0.1.8 → walla_cli-0.1.10}/tests/test_search_api.py +0 -0
  66. {walla_cli-0.1.8 → walla_cli-0.1.10}/tests/test_session_store.py +0 -0
  67. {walla_cli-0.1.8 → walla_cli-0.1.10}/tests/test_shortlist.py +0 -0
  68. {walla_cli-0.1.8 → walla_cli-0.1.10}/tests/test_verdict.py +0 -0
  69. {walla_cli-0.1.8 → walla_cli-0.1.10}/tests/test_wire_contracts.py +0 -0
  70. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/account/__init__.py +0 -0
  71. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/account/actions.py +0 -0
  72. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/account/chat.py +0 -0
  73. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/account/cookies.py +0 -0
  74. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/account/desk.py +0 -0
  75. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/account/inbox.py +0 -0
  76. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/account/login.py +0 -0
  77. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/account/offer_wire.py +0 -0
  78. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/account/prompt.py +0 -0
  79. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/account/session_store.py +0 -0
  80. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/cli/__init__.py +0 -0
  81. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/cli/catch.py +0 -0
  82. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/cli/cmd_account.py +0 -0
  83. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/cli/cmd_desk.py +0 -0
  84. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/cli/cmd_ops.py +0 -0
  85. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/cli/console.py +0 -0
  86. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/core/__init__.py +0 -0
  87. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/core/envelope.py +0 -0
  88. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/core/errors.py +0 -0
  89. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/core/exceptions.py +0 -0
  90. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/core/human_fix.py +0 -0
  91. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/core/path.py +0 -0
  92. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/http/__init__.py +0 -0
  93. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/http/client.py +0 -0
  94. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/http/headers.py +0 -0
  95. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/http/polite.py +0 -0
  96. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/hunter/__init__.py +0 -0
  97. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/hunter/converge.py +0 -0
  98. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/hunter/export.py +0 -0
  99. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/hunter/export_pdf.py +0 -0
  100. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/hunter/negotiate.py +0 -0
  101. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/hunter/profile_store.py +0 -0
  102. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/hunter/pursuits.py +0 -0
  103. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/hunter/shortlist.py +0 -0
  104. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/hunter/stack.py +0 -0
  105. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/hunter/verdict.py +0 -0
  106. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/hunter/watches.py +0 -0
  107. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/mcp/__init__.py +0 -0
  108. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/mcp/_entry.py +0 -0
  109. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/models/__init__.py +0 -0
  110. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/models/account.py +0 -0
  111. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/models/listing.py +0 -0
  112. {walla_cli-0.1.8 → walla_cli-0.1.10}/walla/search/api.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: walla-cli
3
- Version: 0.1.8
3
+ Version: 0.1.10
4
4
  Summary: Wallapop.es CLI - look · offer · talk · sell for humans and agents
5
5
  Project-URL: Homepage, https://github.com/berendgort/wallapop-cli
6
6
  Project-URL: Repository, https://github.com/berendgort/wallapop-cli
@@ -55,12 +55,21 @@ Description-Content-Type: text/markdown
55
55
  Dr. Berend Gort · www.berendgort.dev
56
56
  ```
57
57
 
58
- ![walla banner](docs/assets/walla-github-banner.jpg)
58
+ [![walla sell demo](docs/assets/walla-demo-preview.gif)](https://github.com/berendgort/wallapop-cli/releases/download/demo/walla-demo.mp4)
59
59
 
60
- **walla** is an unofficial Wallapop.es **CLI + MCP + Python library** for Cursor
61
- agents and humans: search listings, negotiate, publish a listing, and follow the inbox.
60
+ ▶ [Play full demo with sound](https://github.com/berendgort/wallapop-cli/releases/download/demo/walla-demo.mp4)
61
+ · Demo music: [Funkee Monkeee](https://mixkit.co/free-stock-music/funkee-monkeee-1140/) by Michael Ramir C.
62
+ ([Mixkit Stock Music Free License](https://mixkit.co/license/#musicFree)).
62
63
 
63
- Not affiliated with Wallapop. Respect ToS and rate limits.
64
+ **walla** turns Claude, GPT, or Cursor into a Wallapop helper that actually
65
+ *does* the work for you. You drop a few photos and answer a couple of
66
+ questions; the agent publishes the listing. When buyers message, it reads
67
+ the chats, replies for you, and negotiates down to your minimum price —
68
+ then shows you the best deal. You only open the Wallapop app to accept.
69
+ No typing chats. No babysitting the inbox.
70
+
71
+ Unofficial Wallapop.es CLI + MCP + Python library. Not affiliated with
72
+ Wallapop. Respect ToS and rate limits.
64
73
 
65
74
  ### What this unlocks
66
75
 
@@ -11,12 +11,21 @@
11
11
  Dr. Berend Gort · www.berendgort.dev
12
12
  ```
13
13
 
14
- ![walla banner](docs/assets/walla-github-banner.jpg)
14
+ [![walla sell demo](docs/assets/walla-demo-preview.gif)](https://github.com/berendgort/wallapop-cli/releases/download/demo/walla-demo.mp4)
15
15
 
16
- **walla** is an unofficial Wallapop.es **CLI + MCP + Python library** for Cursor
17
- agents and humans: search listings, negotiate, publish a listing, and follow the inbox.
16
+ ▶ [Play full demo with sound](https://github.com/berendgort/wallapop-cli/releases/download/demo/walla-demo.mp4)
17
+ · Demo music: [Funkee Monkeee](https://mixkit.co/free-stock-music/funkee-monkeee-1140/) by Michael Ramir C.
18
+ ([Mixkit Stock Music Free License](https://mixkit.co/license/#musicFree)).
18
19
 
19
- Not affiliated with Wallapop. Respect ToS and rate limits.
20
+ **walla** turns Claude, GPT, or Cursor into a Wallapop helper that actually
21
+ *does* the work for you. You drop a few photos and answer a couple of
22
+ questions; the agent publishes the listing. When buyers message, it reads
23
+ the chats, replies for you, and negotiates down to your minimum price —
24
+ then shows you the best deal. You only open the Wallapop app to accept.
25
+ No typing chats. No babysitting the inbox.
26
+
27
+ Unofficial Wallapop.es CLI + MCP + Python library. Not affiliated with
28
+ Wallapop. Respect ToS and rate limits.
20
29
 
21
30
  ### What this unlocks
22
31
 
@@ -29,7 +29,10 @@ Public URL: `https://es.wallapop.com/item/<web_slug>`.
29
29
 
30
30
  ## Categories
31
31
 
32
- `GET https://api.wallapop.com/api/v3/categories` — top-level tree.
32
+ `GET https://api.wallapop.com/api/v3/categories` — nested tree
33
+ (`subcategories[]`). CLI: `walla categories --find escritorio --json` returns
34
+ `leaf_id` + `root_id`. Sell accepts `--category <leaf-or-name>`; `--root` is
35
+ optional.
33
36
 
34
37
  ## Auth
35
38
 
@@ -0,0 +1,59 @@
1
+ {
2
+ "categories": [
3
+ {
4
+ "id": 100,
5
+ "name": "Cars",
6
+ "path": "",
7
+ "vertical_id": "cars",
8
+ "icon": "car",
9
+ "subcategories": [],
10
+ "category_leaf_selection_mandatory": false
11
+ },
12
+ {
13
+ "id": 12467,
14
+ "name": "Hogar y jardín",
15
+ "path": "",
16
+ "vertical_id": "consumer_goods",
17
+ "icon": "home",
18
+ "subcategories": [
19
+ {
20
+ "id": 10125,
21
+ "name": "Muebles y organización",
22
+ "parent_id": 12467,
23
+ "subcategories": [
24
+ {
25
+ "id": 24207,
26
+ "name": "Mesas y mesitas",
27
+ "parent_id": 10125,
28
+ "subcategories": [
29
+ {
30
+ "id": 24208,
31
+ "name": "Escritorios",
32
+ "parent_id": 24207,
33
+ "subcategories": []
34
+ }
35
+ ]
36
+ }
37
+ ]
38
+ }
39
+ ],
40
+ "category_leaf_selection_mandatory": false
41
+ },
42
+ {
43
+ "id": 12579,
44
+ "name": "Deporte y ocio",
45
+ "path": "",
46
+ "vertical_id": "consumer_goods",
47
+ "icon": "ball",
48
+ "subcategories": [
49
+ {
50
+ "id": 10105,
51
+ "name": "Surf y bodyboard",
52
+ "parent_id": 12579,
53
+ "subcategories": []
54
+ }
55
+ ],
56
+ "category_leaf_selection_mandatory": false
57
+ }
58
+ ]
59
+ }
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "walla-cli"
3
- version = "0.1.8"
3
+ version = "0.1.10"
4
4
  description = "Wallapop.es CLI - look · offer · talk · sell for humans and agents"
5
5
  authors = [{ name = "Dr. Berend Gort", email = "berend.gort@gmail.com" }]
6
6
  readme = "README.md"
@@ -13,6 +13,7 @@ license: MIT
13
13
  Do not ask the human to type buyer replies. Desk sends those.
14
14
 
15
15
  ```bash
16
+ walla categories --find escritorio --json
16
17
  walla sell ./shot1.jpg ./shot2.jpg --json
17
18
  ```
18
19
 
@@ -21,13 +22,13 @@ Ask every question in `data.questions`, then publish once:
21
22
  ```bash
22
23
  walla sell ./shot1.jpg ./shot2.jpg \
23
24
  --title "…" --desc "…" --eur 40 \
24
- --category 10105 --root 12579 --condition good \
25
+ --category escritorio --condition good \
25
26
  --floor 35 --yes --json
26
27
  ```
27
28
 
28
- `--floor` is the lowest EUR desk may accept. Omit it to hold the asking price.
29
- Category ids come from `walla categories --json`.
30
-
29
+ `--category` accepts a leaf id **or** name tokens (`escritorio`, `hogar/muebles`).
30
+ `--root` is optional (derived from the leaf). `--floor` is the lowest EUR desk may
31
+ accept. Omit it to hold the asking price.
31
32
  Then:
32
33
 
33
34
  ```bash
@@ -0,0 +1,58 @@
1
+ """Category tree find / root resolve."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+
8
+ import pytest
9
+
10
+ from walla.search.category_find import find_categories, flatten_categories, resolve_root_id
11
+ from walla.search.parse import parse_categories
12
+ from walla.search.pick_category import pick_sell_category
13
+
14
+ FIX = Path(__file__).resolve().parents[1] / "fixtures"
15
+
16
+
17
+ def _roots():
18
+ raw = json.loads((FIX / "categories.json").read_text(encoding="utf-8"))
19
+ return parse_categories(raw)
20
+
21
+
22
+ def test_parse_nested_categories() -> None:
23
+ roots = _roots()
24
+ hogar = next(c for c in roots if c.id == 12467)
25
+ assert hogar.subcategories
26
+ assert flatten_categories(roots)
27
+ assert resolve_root_id(roots, "24208") == "12467"
28
+ assert resolve_root_id(roots, "10105") == "12579"
29
+
30
+
31
+ def test_find_escritorio() -> None:
32
+ hits = find_categories(_roots(), "escritorio")
33
+ assert hits
34
+ assert hits[0].leaf_id == "24208"
35
+ assert hits[0].root_id == "12467"
36
+ assert hits[0].is_leaf
37
+
38
+
39
+ def test_pick_by_name_and_id() -> None:
40
+ roots = _roots()
41
+ by_name = pick_sell_category(roots, category="escritorio")
42
+ assert by_name.leaf_id == "24208"
43
+ assert by_name.root_id == "12467"
44
+ by_id = pick_sell_category(roots, category="24208")
45
+ assert by_id.leaf_id == "24208"
46
+ assert by_id.root_id == "12467"
47
+ forced = pick_sell_category(roots, category="10105", root="12579")
48
+ assert forced.root_id == "12579"
49
+
50
+
51
+ def test_find_empty_and_unknown() -> None:
52
+ with pytest.raises(ValueError, match="empty"):
53
+ find_categories(_roots(), " ")
54
+ assert find_categories(_roots(), "zzzz-no-such") == []
55
+ with pytest.raises(ValueError, match="unknown"):
56
+ resolve_root_id(_roots(), "999999")
57
+ with pytest.raises(ValueError, match="no category matches"):
58
+ pick_sell_category(_roots(), category="zzzz-no-such")
@@ -36,3 +36,5 @@ def test_parse_categories_fixture() -> None:
36
36
  assert len(cats) >= 1
37
37
  assert cats[0].id
38
38
  assert cats[0].name
39
+ hogar = next(c for c in cats if c.id == 12467)
40
+ assert any(s.name == "Muebles y organización" for s in hogar.subcategories)
@@ -14,6 +14,7 @@ from walla.account.sell_wire import (
14
14
  build_item_body,
15
15
  missing_sell_fields,
16
16
  )
17
+ from walla.core.exceptions import WallaHTTPError
17
18
 
18
19
  ROOT = Path(__file__).resolve().parents[1]
19
20
  FIXTURE = ROOT / "fixtures" / "sell_item_request.json"
@@ -130,6 +131,9 @@ def test_publish_listing_mocked(tmp_path: Path) -> None:
130
131
  {"step": {"id": "listing"}},
131
132
  ]
132
133
  fake_http.get.return_value = {}
134
+ listing = MagicMock()
135
+ listing.url = "https://es.wallapop.com/item/test-item-99"
136
+ listing.web_slug = "test-item-99"
133
137
  with (
134
138
  patch("walla.account.sell._auth_client", return_value=fake_http),
135
139
  patch("walla.account.sell.upload_pictures", return_value=1),
@@ -137,6 +141,7 @@ def test_publish_listing_mocked(tmp_path: Path) -> None:
137
141
  "walla.account.sell.create_listing",
138
142
  return_value={"id": "abc123", "flags": {}},
139
143
  ),
144
+ patch("walla.account.sell.get_item", return_value=listing),
140
145
  ):
141
146
  out = publish_listing(
142
147
  [photo],
@@ -150,6 +155,7 @@ def test_publish_listing_mocked(tmp_path: Path) -> None:
150
155
  )
151
156
  assert out["id"] == "abc123"
152
157
  assert out["photos"] == 1
158
+ assert out["url"] == "https://es.wallapop.com/item/test-item-99"
153
159
  assert fake_http.post.call_count == 5
154
160
 
155
161
 
@@ -217,6 +223,9 @@ def test_publish_listing_two_photos(tmp_path: Path) -> None:
217
223
  fake_resp.text = ""
218
224
  fake_session = MagicMock()
219
225
  fake_session.request.return_value = fake_resp
226
+ listing = MagicMock()
227
+ listing.url = "https://es.wallapop.com/item/test-item-99"
228
+ listing.web_slug = "test-item-99"
220
229
  with (
221
230
  patch("walla.account.sell._auth_client", return_value=fake_http),
222
231
  patch("walla.account.sell.upload_pictures", return_value=2),
@@ -224,6 +233,7 @@ def test_publish_listing_two_photos(tmp_path: Path) -> None:
224
233
  "walla.account.sell.create_listing",
225
234
  return_value={"id": "abc123", "flags": {}},
226
235
  ),
236
+ patch("walla.account.sell.get_item", return_value=listing),
227
237
  patch("walla.account.sell._auth_headers", return_value={}),
228
238
  patch("walla.account.sell.requests.Session", return_value=fake_session),
229
239
  patch("walla.account.sell.CurlMime") as mime_cls,
@@ -243,9 +253,27 @@ def test_publish_listing_two_photos(tmp_path: Path) -> None:
243
253
  weight_kg=2,
244
254
  )
245
255
  assert out["photos"] == 2
256
+ assert out["url"] == "https://es.wallapop.com/item/test-item-99"
246
257
  fake_session.request.assert_called()
247
258
 
248
259
 
260
+ def test_public_item_url_uses_slug_not_hash() -> None:
261
+ from walla.account.sell import _public_item_url
262
+
263
+ listing = MagicMock()
264
+ listing.url = ""
265
+ listing.web_slug = "escritorio-madera-1309402890"
266
+ with patch("walla.account.sell.get_item", return_value=listing):
267
+ assert _public_item_url("x6qq0n82pe6y").endswith(
268
+ "/item/escritorio-madera-1309402890"
269
+ )
270
+ with patch(
271
+ "walla.account.sell.get_item",
272
+ side_effect=WallaHTTPError("down", status_code=500),
273
+ ):
274
+ assert _public_item_url("x6qq0n82pe6y").endswith("/item/x6qq0n82pe6y")
275
+
276
+
249
277
  def test_assert_item_body_errors() -> None:
250
278
  with pytest.raises(ValueError, match="missing"):
251
279
  assert_item_body({})