walla-cli 0.1.12__tar.gz → 0.1.14__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 (128) hide show
  1. {walla_cli-0.1.12 → walla_cli-0.1.14}/AGENTS.md +8 -1
  2. {walla_cli-0.1.12 → walla_cli-0.1.14}/PKG-INFO +16 -2
  3. {walla_cli-0.1.12 → walla_cli-0.1.14}/README.md +15 -1
  4. {walla_cli-0.1.12 → walla_cli-0.1.14}/docs/WIRE.md +12 -1
  5. {walla_cli-0.1.12 → walla_cli-0.1.14}/docs/data_engineering_standards.md +3 -1
  6. {walla_cli-0.1.12 → walla_cli-0.1.14}/fixtures/categories.json +36 -0
  7. walla_cli-0.1.14/fixtures/edit_item_request.json +49 -0
  8. {walla_cli-0.1.12 → walla_cli-0.1.14}/fixtures/wire_contracts.json +13 -0
  9. walla_cli-0.1.14/incoming/teach/README.md +3 -0
  10. {walla_cli-0.1.12 → walla_cli-0.1.14}/pyproject.toml +1 -1
  11. {walla_cli-0.1.12 → walla_cli-0.1.14}/tests/test_categories_find.py +11 -0
  12. {walla_cli-0.1.12 → walla_cli-0.1.14}/tests/test_cli_json.py +5 -1
  13. {walla_cli-0.1.12 → walla_cli-0.1.14}/tests/test_core.py +2 -1
  14. walla_cli-0.1.14/tests/test_edit.py +94 -0
  15. walla_cli-0.1.14/tests/test_teach.py +73 -0
  16. {walla_cli-0.1.12 → walla_cli-0.1.14}/tests/test_wire_contracts.py +18 -0
  17. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/__init__.py +1 -1
  18. walla_cli-0.1.14/walla/account/sell_edit.py +106 -0
  19. walla_cli-0.1.14/walla/account/sell_edit_wire.py +85 -0
  20. walla_cli-0.1.14/walla/account/sell_photos.py +56 -0
  21. walla_cli-0.1.14/walla/cli/cmd_edit.py +89 -0
  22. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/cli/cmd_look.py +10 -2
  23. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/cli/cmd_ops.py +6 -1
  24. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/cli/cmd_sell.py +3 -5
  25. walla_cli-0.1.14/walla/cli/cmd_teach.py +53 -0
  26. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/cli/main.py +11 -1
  27. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/core/instruct.py +20 -3
  28. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/core/path.py +10 -0
  29. walla_cli-0.1.14/walla/core/teach.py +138 -0
  30. walla_cli-0.1.14/walla/core/teach_pr.py +199 -0
  31. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/http/client.py +6 -0
  32. walla_cli-0.1.14/walla/http/sign.py +32 -0
  33. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/hunter/shortlist.py +35 -3
  34. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/mcp/server.py +22 -0
  35. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/search/pick_category.py +22 -0
  36. {walla_cli-0.1.12 → walla_cli-0.1.14}/.cursor/rules/quality.mdc +0 -0
  37. {walla_cli-0.1.12 → walla_cli-0.1.14}/.github/workflows/ci.yml +0 -0
  38. {walla_cli-0.1.12 → walla_cli-0.1.14}/.gitignore +0 -0
  39. {walla_cli-0.1.12 → walla_cli-0.1.14}/LICENSE +0 -0
  40. {walla_cli-0.1.12 → walla_cli-0.1.14}/docs/assets/walla-demo-poster.jpg +0 -0
  41. {walla_cli-0.1.12 → walla_cli-0.1.14}/docs/assets/walla-demo-preview.gif +0 -0
  42. {walla_cli-0.1.12 → walla_cli-0.1.14}/docs/assets/walla-demo.mp4 +0 -0
  43. {walla_cli-0.1.12 → walla_cli-0.1.14}/docs/assets/walla-github-banner.jpg +0 -0
  44. {walla_cli-0.1.12 → walla_cli-0.1.14}/docs/code_quality.md +0 -0
  45. {walla_cli-0.1.12 → walla_cli-0.1.14}/docs/mcp.md +0 -0
  46. {walla_cli-0.1.12 → walla_cli-0.1.14}/docs/objective_function.md +0 -0
  47. {walla_cli-0.1.12 → walla_cli-0.1.14}/docs/rate_limit_analysis.md +0 -0
  48. {walla_cli-0.1.12 → walla_cli-0.1.14}/fixtures/item_detail.json +0 -0
  49. {walla_cli-0.1.12 → walla_cli-0.1.14}/fixtures/offer_buyer_request.json +0 -0
  50. {walla_cli-0.1.12 → walla_cli-0.1.14}/fixtures/search_bicicleta.json +0 -0
  51. {walla_cli-0.1.12 → walla_cli-0.1.14}/fixtures/sell_item_request.json +0 -0
  52. {walla_cli-0.1.12 → walla_cli-0.1.14}/fixtures/sell_suggest_draft.json +0 -0
  53. {walla_cli-0.1.12 → walla_cli-0.1.14}/scripts/check_code_quality.py +0 -0
  54. {walla_cli-0.1.12 → walla_cli-0.1.14}/scripts/dogfood_run.py +0 -0
  55. {walla_cli-0.1.12 → walla_cli-0.1.14}/scripts/probe_rate.py +0 -0
  56. {walla_cli-0.1.12 → walla_cli-0.1.14}/skills/walla/SKILL.md +0 -0
  57. {walla_cli-0.1.12 → walla_cli-0.1.14}/skills/walla-buy/SKILL.md +0 -0
  58. {walla_cli-0.1.12 → walla_cli-0.1.14}/skills/walla-sell/SKILL.md +0 -0
  59. {walla_cli-0.1.12 → walla_cli-0.1.14}/tests/test_account.py +0 -0
  60. {walla_cli-0.1.12 → walla_cli-0.1.14}/tests/test_account_more.py +0 -0
  61. {walla_cli-0.1.12 → walla_cli-0.1.14}/tests/test_catch.py +0 -0
  62. {walla_cli-0.1.12 → walla_cli-0.1.14}/tests/test_chat_open.py +0 -0
  63. {walla_cli-0.1.12 → walla_cli-0.1.14}/tests/test_desk.py +0 -0
  64. {walla_cli-0.1.12 → walla_cli-0.1.14}/tests/test_edges.py +0 -0
  65. {walla_cli-0.1.12 → walla_cli-0.1.14}/tests/test_export.py +0 -0
  66. {walla_cli-0.1.12 → walla_cli-0.1.14}/tests/test_http_more.py +0 -0
  67. {walla_cli-0.1.12 → walla_cli-0.1.14}/tests/test_live.py +0 -0
  68. {walla_cli-0.1.12 → walla_cli-0.1.14}/tests/test_negotiate.py +0 -0
  69. {walla_cli-0.1.12 → walla_cli-0.1.14}/tests/test_parsers.py +0 -0
  70. {walla_cli-0.1.12 → walla_cli-0.1.14}/tests/test_polite.py +0 -0
  71. {walla_cli-0.1.12 → walla_cli-0.1.14}/tests/test_regressions.py +0 -0
  72. {walla_cli-0.1.12 → walla_cli-0.1.14}/tests/test_search_api.py +0 -0
  73. {walla_cli-0.1.12 → walla_cli-0.1.14}/tests/test_sell.py +0 -0
  74. {walla_cli-0.1.12 → walla_cli-0.1.14}/tests/test_session_store.py +0 -0
  75. {walla_cli-0.1.12 → walla_cli-0.1.14}/tests/test_shortlist.py +0 -0
  76. {walla_cli-0.1.12 → walla_cli-0.1.14}/tests/test_suggest_watch.py +0 -0
  77. {walla_cli-0.1.12 → walla_cli-0.1.14}/tests/test_verdict.py +0 -0
  78. {walla_cli-0.1.12 → walla_cli-0.1.14}/uv.lock +0 -0
  79. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/account/__init__.py +0 -0
  80. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/account/actions.py +0 -0
  81. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/account/chat.py +0 -0
  82. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/account/cookies.py +0 -0
  83. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/account/desk.py +0 -0
  84. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/account/desk_watch.py +0 -0
  85. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/account/inbox.py +0 -0
  86. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/account/login.py +0 -0
  87. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/account/offer_wire.py +0 -0
  88. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/account/prompt.py +0 -0
  89. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/account/sell.py +0 -0
  90. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/account/sell_steps.py +0 -0
  91. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/account/sell_suggest.py +0 -0
  92. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/account/sell_upload.py +0 -0
  93. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/account/sell_wire.py +0 -0
  94. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/account/session_store.py +0 -0
  95. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/cli/__init__.py +0 -0
  96. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/cli/banner.py +0 -0
  97. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/cli/catch.py +0 -0
  98. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/cli/cmd_account.py +0 -0
  99. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/cli/cmd_desk.py +0 -0
  100. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/cli/console.py +0 -0
  101. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/core/__init__.py +0 -0
  102. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/core/envelope.py +0 -0
  103. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/core/errors.py +0 -0
  104. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/core/exceptions.py +0 -0
  105. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/core/human_fix.py +0 -0
  106. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/http/__init__.py +0 -0
  107. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/http/headers.py +0 -0
  108. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/http/polite.py +0 -0
  109. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/hunter/__init__.py +0 -0
  110. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/hunter/converge.py +0 -0
  111. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/hunter/export.py +0 -0
  112. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/hunter/export_pdf.py +0 -0
  113. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/hunter/negotiate.py +0 -0
  114. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/hunter/profile_store.py +0 -0
  115. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/hunter/pursuits.py +0 -0
  116. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/hunter/stack.py +0 -0
  117. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/hunter/verdict.py +0 -0
  118. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/hunter/watches.py +0 -0
  119. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/mcp/__init__.py +0 -0
  120. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/mcp/_entry.py +0 -0
  121. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/models/__init__.py +0 -0
  122. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/models/account.py +0 -0
  123. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/models/listing.py +0 -0
  124. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/models/profile.py +0 -0
  125. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/search/__init__.py +0 -0
  126. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/search/api.py +0 -0
  127. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/search/category_find.py +0 -0
  128. {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/search/parse.py +0 -0
@@ -57,7 +57,9 @@ Ranks: models 10 → exceptions 15 → http 20 → core 30 → search/account/hu
57
57
 
58
58
  - **Do** keep files <= 250 LOC.
59
59
  - **Do** fail closed; typed `error_type`.
60
- - **Do** require `--yes` for bare say/offer/sell/unsell. `pursue` and `desk` send negotiation chat without a per-message prompt. Never pay.
60
+ - **Do** require `--yes` for bare say/offer/sell/unsell/edit. `pursue` and `desk` send negotiation chat without a per-message prompt. Never pay.
61
+ - **Do** use `walla edit <id> --eur N --yes` to change price/desc (no unsell+republish).
62
+ - **Do** pass leaf id or `motor accesorios` when `--category Accesorios` is ambiguous.
61
63
  - **Don't** commit session.json, cookies, or tokens.
62
64
  - **Don't** invent wire shapes without a fixture.
63
65
  - **Don't** post buyer offers as `{item_id, amount, currency}` (HTTP 400).
@@ -65,3 +67,8 @@ Ranks: models 10 → exceptions 15 → http 20 → core 30 → search/account/hu
65
67
  - **Don't** create listings without `Accept: application/vnd.upload-v2+json`
66
68
  (HTTP 405). Use `walla sell` / `fixtures/sell_item_request.json`.
67
69
  - **Don't** require `.env` credentials; login is cookie paste.
70
+ - **Don't** nag about transcripts on search/desk/pursue. First-use notice once.
71
+ At 20/40/60 prompts ask yes/no, then A-E (gh/git/browser/hub/manual).
72
+ If they ask to upload a transcript, run `walla teach` immediately.
73
+ `walla teach FILE --method a --yes` opens a PR; on failure use
74
+ https://github.com/berendgort/wallapop-cli/compare and help them finish.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: walla-cli
3
- Version: 0.1.12
3
+ Version: 0.1.14
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
@@ -198,7 +198,8 @@ walla doctor --json
198
198
  | `walla negotiate` | Win-win Spanish draft (never sends) |
199
199
  | `walla inbox` / `thread` / `say` | Talk (`say` needs `--yes`) |
200
200
  | `walla offer` | Formal offer (needs `--yes`) |
201
- | `walla sell` / `unsell` | Publish or delete a listing (`--yes`) |
201
+ | `walla sell` / `unsell` / `edit` | Publish, edit, or delete (`--yes`) |
202
+ | `walla teach` | Park a redacted agent transcript locally |
202
203
  | `walla fav list\|add\|rm` | Favorites |
203
204
  | `walla instruct` / `doctor` | Agent recipe + health |
204
205
 
@@ -225,6 +226,19 @@ python scripts/check_code_quality.py
225
226
 
226
227
  Read [`AGENTS.md`](AGENTS.md) before extending.
227
228
 
229
+ ## Help improve walla
230
+
231
+ Long agent chats help. After you confirm, pick a PR method:
232
+
233
+ ```bash
234
+ walla teach ./cursor-export.md --method a --yes --json # A gh B git C browser D hub E manual
235
+ ```
236
+
237
+ Cookies/tokens are stripped first. If `gh`/`git`/`hub` is missing, open
238
+ https://github.com/berendgort/wallapop-cli/compare and file the PR by hand
239
+ (parked file is in `~/.config/walla/teach/`). Agents ask once on first use,
240
+ then at 20 / 40 / 60 prompts — never on every search.
241
+
228
242
  ## Disclaimer
229
243
 
230
244
  Unofficial. Not affiliated with Wallapop. Personal / research use.
@@ -154,7 +154,8 @@ walla doctor --json
154
154
  | `walla negotiate` | Win-win Spanish draft (never sends) |
155
155
  | `walla inbox` / `thread` / `say` | Talk (`say` needs `--yes`) |
156
156
  | `walla offer` | Formal offer (needs `--yes`) |
157
- | `walla sell` / `unsell` | Publish or delete a listing (`--yes`) |
157
+ | `walla sell` / `unsell` / `edit` | Publish, edit, or delete (`--yes`) |
158
+ | `walla teach` | Park a redacted agent transcript locally |
158
159
  | `walla fav list\|add\|rm` | Favorites |
159
160
  | `walla instruct` / `doctor` | Agent recipe + health |
160
161
 
@@ -181,6 +182,19 @@ python scripts/check_code_quality.py
181
182
 
182
183
  Read [`AGENTS.md`](AGENTS.md) before extending.
183
184
 
185
+ ## Help improve walla
186
+
187
+ Long agent chats help. After you confirm, pick a PR method:
188
+
189
+ ```bash
190
+ walla teach ./cursor-export.md --method a --yes --json # A gh B git C browser D hub E manual
191
+ ```
192
+
193
+ Cookies/tokens are stripped first. If `gh`/`git`/`hub` is missing, open
194
+ https://github.com/berendgort/wallapop-cli/compare and file the PR by hand
195
+ (parked file is in `~/.config/walla/teach/`). Agents ask once on first use,
196
+ then at 20 / 40 / 60 prompts — never on every search.
197
+
184
198
  ## Disclaimer
185
199
 
186
200
  Unofficial. Not affiliated with Wallapop. Personal / research use.
@@ -81,14 +81,25 @@ Browser: `https://es.wallapop.com/app/catalog/upload/consumer-goods`.
81
81
  8. Create: `POST /api/v3/items` multipart `image` + `item` (JSON string).
82
82
  Header **`Accept: application/vnd.upload-v2+json`** (without it → HTTP 405).
83
83
  Extra photos: `POST /api/v3/items/{id}/picture2` multipart `image` + `order`.
84
- 9. Delete: `DELETE /api/v3/items/{id}` → 204.
84
+ 9. Edit owned item: `PUT /api/v3/items/{id}` JSON body (same attribute shape as
85
+ create, plus `pictures: [{id, order}]`). Headers: Bearer,
86
+ **`Accept: application/vnd.upload-v2+json`**, **`X-Signature`** +
87
+ **`Timestamp`** (HMAC of `PUT|{absolute_url}|{ts_ms}|`). HTTP **204**.
88
+ Fixture: `fixtures/edit_item_request.json` / `walla.account.sell_edit_wire`.
89
+ CLI: `walla edit <id> --eur N --desc '…' --yes`.
90
+ 10. Delete: `DELETE /api/v3/items/{id}` → 204.
85
91
 
86
92
  Fixture: `fixtures/sell_item_request.json` / `walla.account.sell_wire.build_item_body`.
93
+ HEIC/HEIF sell photos are converted to JPEG via ImageMagick (`magick`/`convert`)
94
+ in `walla.account.sell_photos` before upload.
87
95
 
88
96
  Every CLI call (method, path, auth, body) is listed in `fixtures/wire_contracts.json`.
89
97
  `scripts/check_code_quality.py` fails if `walla/account`, `walla/search`, or `walla/http`
90
98
  grows a `/api/` or `/bff/` literal that the catalog does not list.
91
99
  CLI: `walla sell photo.jpg … --yes` (draft without `--yes`).
100
+ Bare `Accesorios` is ambiguous across Motor / Cocina — pass leaf id or
101
+ `motor accesorios`. Sticky `must_match` that empties a shortlist triggers a soft
102
+ fallback + `mandate_note` (`walla setup --must ''` to clear).
92
103
 
93
104
  Writes that fail closed as `error_type: unsupported` until fixtures exist.
94
105
 
@@ -15,8 +15,9 @@ Code shape: [`code_quality.md`](code_quality.md). Wire truth: [`WIRE.md`](WIRE.m
15
15
 
16
16
  | Mode | What | Mutability | Clock |
17
17
  |---|---|---|---|
18
- | Working | `profile.json`, `watches.json`, `session.json` | mutable | user lifetime |
18
+ | Working | `profile.json`, `watches.json`, `session.json`, `teach.json` | mutable | user lifetime |
19
19
  | Archive | redacted fixtures under `fixtures/` | append on capture | repo life |
20
+ | Teach drop | `~/.config/walla/teach/*.md` | append | user opt-in |
20
21
  | Live | `api.wallapop.com` responses | read | request |
21
22
 
22
23
  There is no database in v1. Do not stand up Postgres, Redis, or a second
@@ -37,6 +38,7 @@ file for the same fields is a defect.
37
38
  - Profile: one home point, one search radius, one pickup radius.
38
39
  - Watch: one keyword query + optional filters; seen listing ids are a set.
39
40
  - Session: tokens / cookie fields only. Never the password.
41
+ - Teach: `teach.json` (first-notice clock) and one redacted file per `walla teach`.
40
42
  - Fixture: one captured response shape, secrets redacted, `schema_version` in
41
43
  the companion parser module docstring.
42
44
 
@@ -35,6 +35,42 @@
35
35
  ]
36
36
  }
37
37
  ]
38
+ },
39
+ {
40
+ "id": 12484,
41
+ "name": "Cocina",
42
+ "parent_id": 12467,
43
+ "subcategories": [
44
+ {
45
+ "id": 24235,
46
+ "name": "Accesorios",
47
+ "parent_id": 12484,
48
+ "subcategories": []
49
+ }
50
+ ]
51
+ }
52
+ ],
53
+ "category_leaf_selection_mandatory": false
54
+ },
55
+ {
56
+ "id": 12800,
57
+ "name": "Motor y accesorios",
58
+ "path": "",
59
+ "vertical_id": "consumer_goods",
60
+ "icon": "engine",
61
+ "subcategories": [
62
+ {
63
+ "id": 10167,
64
+ "name": "Recambios de coches y furgonetas",
65
+ "parent_id": 12800,
66
+ "subcategories": [
67
+ {
68
+ "id": 10328,
69
+ "name": "Accesorios",
70
+ "parent_id": 10167,
71
+ "subcategories": []
72
+ }
73
+ ]
38
74
  }
39
75
  ],
40
76
  "category_leaf_selection_mandatory": false
@@ -0,0 +1,49 @@
1
+ {
2
+ "endpoint": "PUT /api/v3/items/{id}",
3
+ "accept": "application/vnd.upload-v2+json",
4
+ "auth": "Bearer + X-Signature + Timestamp",
5
+ "signature": {
6
+ "message": "PUT|{absolute_url}|{timestamp_ms}|",
7
+ "note": "HMAC-SHA256 with Wallapop consumer key; required for edit"
8
+ },
9
+ "required_keys": [
10
+ "attributes",
11
+ "category_leaf_id",
12
+ "apply_discount",
13
+ "delivery",
14
+ "location",
15
+ "pictures"
16
+ ],
17
+ "attribute_required_keys": [
18
+ "title",
19
+ "description",
20
+ "price_amount",
21
+ "condition"
22
+ ],
23
+ "example": {
24
+ "attributes": {
25
+ "title": "TEST walla CLI edit - borrar",
26
+ "description": "Anuncio de prueba edit. NO COMPRAR.",
27
+ "price_amount": 9,
28
+ "condition": "good"
29
+ },
30
+ "category_leaf_id": "10105",
31
+ "apply_discount": false,
32
+ "delivery": {
33
+ "allowed_by_user": false,
34
+ "max_weight_kg": null,
35
+ "cost_configuration_id": null
36
+ },
37
+ "location": {
38
+ "latitude": 41.3874,
39
+ "longitude": 2.1686,
40
+ "approximated": true
41
+ },
42
+ "pictures": [
43
+ {
44
+ "id": 0,
45
+ "order": 0
46
+ }
47
+ ]
48
+ }
49
+ }
@@ -197,6 +197,19 @@
197
197
  "multipart": ["image", "order"],
198
198
  "ok_status": [200, 201, 204]
199
199
  },
200
+ {
201
+ "id": "sell_edit",
202
+ "cli": "edit",
203
+ "method": "PUT",
204
+ "path": "/api/v3/items/{id}",
205
+ "needle": "/api/v3/items/",
206
+ "auth": true,
207
+ "accept": "application/vnd.upload-v2+json",
208
+ "signed": true,
209
+ "ok_status": [200, 201, 204],
210
+ "fixture": "edit_item_request.json",
211
+ "known_bad": [{"status": 404, "cause": "missing X-Signature / upload-v2 Accept"}]
212
+ },
200
213
  {
201
214
  "id": "unsell",
202
215
  "cli": "unsell",
@@ -0,0 +1,3 @@
1
+ # Incoming teach drops
2
+
3
+ Redacted agent transcripts from `walla teach --method`. One markdown file per PR.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "walla-cli"
3
- version = "0.1.12"
3
+ version = "0.1.14"
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"
@@ -48,6 +48,17 @@ def test_pick_by_name_and_id() -> None:
48
48
  assert forced.root_id == "12579"
49
49
 
50
50
 
51
+ def test_accesorios_ambiguous_unless_disambiguated() -> None:
52
+ roots = _roots()
53
+ with pytest.raises(ValueError, match="ambiguous"):
54
+ pick_sell_category(roots, category="Accesorios")
55
+ motor = pick_sell_category(roots, category="motor accesorios")
56
+ assert motor.leaf_id == "10328"
57
+ assert motor.root_id == "12800"
58
+ by_id = pick_sell_category(roots, category="10328")
59
+ assert by_id.leaf_id == "10328"
60
+
61
+
51
62
  def test_find_empty_and_unknown() -> None:
52
63
  with pytest.raises(ValueError, match="empty"):
53
64
  find_categories(_roots(), " ")
@@ -40,12 +40,16 @@ def test_rate_limit_fields() -> None:
40
40
  assert p["retry_after_s"] == 2.0
41
41
 
42
42
 
43
- def test_instruct_json() -> None:
43
+ def test_instruct_json(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
44
+ monkeypatch.setenv("WALLA_CONFIG_DIR", str(tmp_path))
44
45
  result = runner.invoke(app, ["instruct", "--json"])
45
46
  assert result.exit_code == 0
46
47
  data = json.loads(result.stdout)
47
48
  assert data["ok"] is True
48
49
  assert data["data"]["name"] == "walla"
50
+ assert data["data"]["teach"]["first_notice"]
51
+ again = runner.invoke(app, ["instruct", "--json"])
52
+ assert json.loads(again.stdout)["data"]["teach"]["first_notice"] is None
49
53
 
50
54
 
51
55
  def test_doctor_never_echoes_secrets(monkeypatch: pytest.MonkeyPatch) -> None:
@@ -14,7 +14,7 @@ from walla.core.exceptions import (
14
14
  WallaParseError,
15
15
  WallaUnsupportedError,
16
16
  )
17
- from walla.core.path import config_dir, profile_path
17
+ from walla.core.path import config_dir, profile_path, teach_dir
18
18
  from walla.http.polite import reset_polite
19
19
  from walla.hunter.profile_store import parse_intake
20
20
 
@@ -32,6 +32,7 @@ def test_paths(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
32
32
  monkeypatch.setenv("WALLA_CONFIG_DIR", str(tmp_path))
33
33
  assert config_dir() == tmp_path
34
34
  assert profile_path() == tmp_path / "profile.json"
35
+ assert teach_dir() == tmp_path / "teach"
35
36
 
36
37
 
37
38
  def test_parse_intake(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
@@ -0,0 +1,94 @@
1
+ """Edit-item fixture + soft shortlist / HEIC prep."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from unittest.mock import MagicMock, patch
8
+
9
+ import pytest
10
+
11
+ from walla.account.sell_edit_wire import assert_edit_body, build_edit_body
12
+ from walla.account.sell_photos import prepare_sell_photos
13
+ from walla.http.sign import sign_request
14
+ from walla.hunter.shortlist import soft_shortlist
15
+
16
+ ROOT = Path(__file__).resolve().parents[1]
17
+ FIX = ROOT / "fixtures" / "edit_item_request.json"
18
+
19
+
20
+ def test_edit_fixture_matches_builder() -> None:
21
+ meta = json.loads(FIX.read_text(encoding="utf-8"))
22
+ example = meta["example"]
23
+ assert_edit_body(example)
24
+ body = build_edit_body(
25
+ title=example["attributes"]["title"],
26
+ description=example["attributes"]["description"],
27
+ price_eur=float(example["attributes"]["price_amount"]),
28
+ category_leaf_id=str(example["category_leaf_id"]),
29
+ condition=example["attributes"]["condition"],
30
+ lat=float(example["location"]["latitude"]),
31
+ lon=float(example["location"]["longitude"]),
32
+ pictures=list(example["pictures"]),
33
+ shipping=False,
34
+ )
35
+ for key in meta["required_keys"]:
36
+ assert key in body
37
+ assert body["pictures"][0]["order"] == 0
38
+
39
+
40
+ def test_sign_request_stable() -> None:
41
+ sig = sign_request(
42
+ method="PUT",
43
+ url="https://api.wallapop.com/api/v3/items/abc",
44
+ timestamp_ms="1700000000000",
45
+ )
46
+ assert isinstance(sig, str) and len(sig) > 20
47
+
48
+
49
+ def test_soft_shortlist_mandate_fallback() -> None:
50
+ rows = [
51
+ {
52
+ "id": "a",
53
+ "title": "ski jacket",
54
+ "verdict": "PASS",
55
+ "price": {"amount": 50},
56
+ "url": "https://es.wallapop.com/item/a",
57
+ },
58
+ {
59
+ "id": "b",
60
+ "title": "other",
61
+ "verdict": "PASS",
62
+ "price": {"amount": 40},
63
+ "url": "https://es.wallapop.com/item/b",
64
+ },
65
+ ]
66
+ hard, note = soft_shortlist(rows, must_match=["north", "reach"])
67
+ assert hard
68
+ assert note is not None
69
+ assert note["mandate_emptied"] is True
70
+ assert [r["id"] for r in hard] == ["b", "a"]
71
+
72
+
73
+ def test_prepare_rejects_unknown(tmp_path: Path) -> None:
74
+ bad = tmp_path / "x.gif"
75
+ bad.write_bytes(b"GIF")
76
+ with pytest.raises(ValueError, match="unsupported"):
77
+ prepare_sell_photos([bad])
78
+
79
+
80
+ def test_prepare_heic_uses_convert(tmp_path: Path) -> None:
81
+ src = tmp_path / "shot.HEIC"
82
+ src.write_bytes(b"heic")
83
+ dest = tmp_path / "shot.jpg"
84
+
85
+ def _fake_run(cmd: list[str], **_: object) -> MagicMock:
86
+ dest.write_bytes(b"\xff\xd8\xff\xd9")
87
+ return MagicMock(returncode=0, stderr="", stdout="")
88
+
89
+ with (
90
+ patch("walla.account.sell_photos.shutil.which", return_value="convert"),
91
+ patch("walla.account.sell_photos.subprocess.run", side_effect=_fake_run),
92
+ ):
93
+ out = prepare_sell_photos([src])
94
+ assert out == [dest]
@@ -0,0 +1,73 @@
1
+ """Local transcript parking (no Wallapop wire)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import pytest
8
+ from pytest import MonkeyPatch
9
+
10
+ from walla.core.teach import (
11
+ FIRST_NOTICE,
12
+ park_transcript,
13
+ redact_secrets,
14
+ should_ask_feedback,
15
+ teach_payload,
16
+ )
17
+ from walla.core.teach_pr import MANUAL_PR, plan_pr, ship_pr
18
+
19
+
20
+ def test_redact_jwt_and_cookie() -> None:
21
+ blob, n = redact_secrets(
22
+ "cookie __Secure-next-auth.session-token=abc.def\n"
23
+ "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.aaa.bbb"
24
+ )
25
+ assert n == 2
26
+ assert "[redacted]" in blob
27
+ assert "session-token=abc" not in blob
28
+
29
+
30
+ def test_park_and_first_notice(tmp_path: Path, monkeypatch: MonkeyPatch) -> None:
31
+ monkeypatch.setenv("WALLA_CONFIG_DIR", str(tmp_path))
32
+ first = teach_payload()
33
+ assert first["first_notice"] == FIRST_NOTICE
34
+ second = teach_payload()
35
+ assert second["first_notice"] is None
36
+ parked = park_transcript(
37
+ "hello Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.aaa.bbb",
38
+ name="hunt",
39
+ )
40
+ path = Path(str(parked["path"]))
41
+ assert path.is_file()
42
+ assert path.parent == tmp_path / "teach"
43
+ assert parked["redacted"] >= 1
44
+ assert "eyJhbGci" not in path.read_text(encoding="utf-8")
45
+
46
+
47
+ def test_should_ask_every_twenty() -> None:
48
+ assert should_ask_feedback(19) is False
49
+ assert should_ask_feedback(20) is True
50
+ assert should_ask_feedback(21, last_asked_band=20) is False
51
+ assert should_ask_feedback(40, last_asked_band=20) is True
52
+ assert should_ask_feedback(40, last_asked_band=40) is False
53
+ assert should_ask_feedback(60, last_asked_band=40) is True
54
+
55
+
56
+ def test_pr_menu_and_draft(tmp_path: Path, monkeypatch: MonkeyPatch) -> None:
57
+ monkeypatch.setenv("WALLA_CONFIG_DIR", str(tmp_path))
58
+ payload = teach_payload()
59
+ ids = [m["id"] for m in payload["methods"]]
60
+ assert ids == ["A", "B", "C", "D", "E"]
61
+ assert payload["manual_pr"] == MANUAL_PR
62
+ draft = plan_pr("a")
63
+ assert draft["method"] == "a"
64
+ assert "github.com" in draft["manual_pr"]
65
+ parked = park_transcript("hello kite hunt", name="x")
66
+ pr = ship_pr(Path(str(parked["path"])), method="e", yes=False)
67
+ assert pr["status"] == "draft"
68
+ assert pr["manual_pr"] == MANUAL_PR
69
+ manual = ship_pr(Path(str(parked["path"])), method="E", yes=True)
70
+ assert manual["status"] == "manual"
71
+ assert manual["open"] == MANUAL_PR
72
+ with pytest.raises(ValueError, match="method"):
73
+ plan_pr("z")
@@ -17,6 +17,7 @@ from walla.account.inbox import list_conversations_raw
17
17
  from walla.account.login import login_password, mint_access_token, refresh_session, whoami
18
18
  from walla.account.offer_wire import assert_offer_body
19
19
  from walla.account.sell import delete_listing, publish_listing
20
+ from walla.account.sell_edit import edit_listing
20
21
  from walla.account.sell_wire import assert_item_body, build_item_body
21
22
  from walla.core.exceptions import WallaAuthError, WallaHTTPError, WallaUnsupportedError
22
23
  from walla.http.client import HttpClient
@@ -244,6 +245,20 @@ def test_every_contract_is_sent(
244
245
  monkeypatch.setattr("walla.account.sell.requests.Session", lambda: _Sess())
245
246
  monkeypatch.setattr("walla.account.sell.CurlMime.addpart", _addpart)
246
247
  monkeypatch.setattr("walla.account.sell.CurlMime.close", lambda _s: None)
248
+ monkeypatch.setattr("walla.account.sell_edit.requests.request", _Sess().request)
249
+ monkeypatch.setattr(
250
+ "walla.account.sell_edit.fetch_owned_item",
251
+ lambda _id: {
252
+ "title": {"original": "Cofre test"},
253
+ "description": {"original": "desc"},
254
+ "price": {"cash": {"amount": 79.0}},
255
+ "type_attributes": {"condition": {"value": "good"}},
256
+ "taxonomy": [{"id": 10328}],
257
+ "location": {"latitude": 41.39, "longitude": 2.17},
258
+ "shipping": {"user_allows_shipping": False},
259
+ "images": [{"id": 1}],
260
+ },
261
+ )
247
262
  sess = SessionData(
248
263
  access_token="tok",
249
264
  device_id="dev-contract",
@@ -302,6 +317,9 @@ def test_every_contract_is_sent(
302
317
  lon=2.17,
303
318
  )
304
319
  assert created["id"] == "item123"
320
+ edited = edit_listing("item123", price_eur=49.0)
321
+ assert edited["edited"] is True
322
+ assert edited["price_eur"] == 49.0
305
323
  delete_listing("item123", client=rec)
306
324
 
307
325
  item_parts = [
@@ -4,4 +4,4 @@ from __future__ import annotations
4
4
 
5
5
  __all__ = ("__version__",)
6
6
 
7
- __version__ = "0.1.12"
7
+ __version__ = "0.1.14"
@@ -0,0 +1,106 @@
1
+ """Edit an owned listing (PUT /api/v3/items/{id} + X-Signature)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any
7
+
8
+ from curl_cffi import requests
9
+
10
+ from walla.account.sell_edit_wire import build_edit_body
11
+ from walla.account.sell_steps import auth_client, auth_headers
12
+ from walla.core.exceptions import WallaHTTPError, WallaUnsupportedError
13
+ from walla.http.headers import API_BASE
14
+ from walla.http.polite import wait_turn
15
+ from walla.http.sign import signed_headers
16
+
17
+ __all__ = ("edit_listing", "fetch_owned_item")
18
+
19
+
20
+ def fetch_owned_item(item_id: str) -> dict[str, Any]:
21
+ """GET /api/v3/items/{id} with auth (seller detail shape)."""
22
+ raw = auth_client().get(f"/api/v3/items/{item_id}", auth=True)
23
+ if not isinstance(raw, dict):
24
+ raise WallaUnsupportedError("item detail returned non-object")
25
+ return raw
26
+
27
+
28
+ def edit_listing(
29
+ item_id: str,
30
+ *,
31
+ title: str | None = None,
32
+ description: str | None = None,
33
+ price_eur: float | None = None,
34
+ shipping: bool | None = None,
35
+ weight_kg: float | None = None,
36
+ ) -> dict[str, Any]:
37
+ """Patch title/description/price/shipping on an owned item. Needs --yes."""
38
+ item = fetch_owned_item(item_id)
39
+ cur_title = _orig(item.get("title"))
40
+ cur_desc = _orig(item.get("description"))
41
+ price_block = item.get("price") or {}
42
+ cash = price_block.get("cash") if isinstance(price_block, dict) else None
43
+ cur_price = float((cash or {}).get("amount") or 0)
44
+ cond = ((item.get("type_attributes") or {}).get("condition") or {}).get("value")
45
+ if not cond:
46
+ raise WallaUnsupportedError("item missing type_attributes.condition")
47
+ tax = item.get("taxonomy") or []
48
+ if not tax:
49
+ raise WallaUnsupportedError("item missing taxonomy leaf")
50
+ leaf = str(tax[-1]["id"])
51
+ loc = item.get("location") or {}
52
+ ship = item.get("shipping") or {}
53
+ allowed = bool(ship.get("user_allows_shipping"))
54
+ pics = [
55
+ {"id": img["id"], "order": i}
56
+ for i, img in enumerate(item.get("images") or [])
57
+ if isinstance(img, dict) and "id" in img
58
+ ]
59
+ body = build_edit_body(
60
+ title=title if title is not None else cur_title,
61
+ description=description if description is not None else cur_desc,
62
+ price_eur=float(price_eur) if price_eur is not None else cur_price,
63
+ category_leaf_id=leaf,
64
+ condition=str(cond),
65
+ lat=float(loc["latitude"]),
66
+ lon=float(loc["longitude"]),
67
+ pictures=pics,
68
+ shipping=allowed if shipping is None else bool(shipping),
69
+ max_weight_kg=weight_kg,
70
+ )
71
+ _put_edit(item_id, body)
72
+ return {
73
+ "id": item_id,
74
+ "edited": True,
75
+ "title": body["attributes"]["title"],
76
+ "price_eur": body["attributes"]["price_amount"],
77
+ "shipping": body["delivery"]["allowed_by_user"],
78
+ }
79
+
80
+
81
+ def _orig(block: Any) -> str:
82
+ if isinstance(block, dict):
83
+ return str(block.get("original") or "")
84
+ return str(block or "")
85
+
86
+
87
+ def _put_edit(item_id: str, body: dict[str, Any]) -> None:
88
+ url = f"{API_BASE}/api/v3/items/{item_id}"
89
+ headers = {k: v for k, v in auth_headers().items() if k.lower() != "content-type"}
90
+ headers["Accept"] = "application/vnd.upload-v2+json"
91
+ headers["Content-Type"] = "application/json"
92
+ headers = signed_headers(method="PUT", url=url, base=headers)
93
+ wait_turn()
94
+ resp: Any = requests.request(
95
+ "PUT",
96
+ url,
97
+ headers=headers,
98
+ data=json.dumps(body).encode(),
99
+ impersonate="chrome",
100
+ timeout=30,
101
+ )
102
+ if resp.status_code not in (200, 201, 204):
103
+ raise WallaHTTPError(
104
+ f"edit item HTTP {resp.status_code}: {(resp.text or '')[:200]}",
105
+ status_code=resp.status_code,
106
+ )