walla-cli 0.1.11__tar.gz → 0.1.13__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.
- {walla_cli-0.1.11 → walla_cli-0.1.13}/AGENTS.md +5 -1
- {walla_cli-0.1.11 → walla_cli-0.1.13}/PKG-INFO +1 -1
- {walla_cli-0.1.11 → walla_cli-0.1.13}/docs/WIRE.md +14 -2
- {walla_cli-0.1.11 → walla_cli-0.1.13}/fixtures/categories.json +36 -0
- walla_cli-0.1.13/fixtures/edit_item_request.json +49 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/fixtures/wire_contracts.json +13 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/pyproject.toml +1 -1
- {walla_cli-0.1.11 → walla_cli-0.1.13}/skills/walla-sell/SKILL.md +3 -2
- {walla_cli-0.1.11 → walla_cli-0.1.13}/tests/test_categories_find.py +11 -0
- walla_cli-0.1.13/tests/test_edit.py +94 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/tests/test_sell.py +57 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/tests/test_wire_contracts.py +18 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/__init__.py +1 -1
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/account/sell.py +58 -50
- walla_cli-0.1.13/walla/account/sell_edit.py +106 -0
- walla_cli-0.1.13/walla/account/sell_edit_wire.py +85 -0
- walla_cli-0.1.13/walla/account/sell_photos.py +56 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/account/sell_suggest.py +1 -1
- walla_cli-0.1.13/walla/account/sell_upload.py +51 -0
- walla_cli-0.1.13/walla/cli/cmd_edit.py +89 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/cli/cmd_look.py +10 -2
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/cli/cmd_sell.py +56 -23
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/cli/main.py +2 -1
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/core/instruct.py +8 -4
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/http/client.py +6 -0
- walla_cli-0.1.13/walla/http/sign.py +32 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/hunter/shortlist.py +35 -3
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/search/category_find.py +4 -4
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/search/pick_category.py +22 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/.cursor/rules/quality.mdc +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/.github/workflows/ci.yml +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/.gitignore +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/LICENSE +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/README.md +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/docs/assets/walla-demo-poster.jpg +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/docs/assets/walla-demo-preview.gif +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/docs/assets/walla-demo.mp4 +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/docs/assets/walla-github-banner.jpg +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/docs/code_quality.md +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/docs/data_engineering_standards.md +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/docs/mcp.md +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/docs/objective_function.md +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/docs/rate_limit_analysis.md +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/fixtures/item_detail.json +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/fixtures/offer_buyer_request.json +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/fixtures/search_bicicleta.json +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/fixtures/sell_item_request.json +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/fixtures/sell_suggest_draft.json +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/scripts/check_code_quality.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/scripts/dogfood_run.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/scripts/probe_rate.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/skills/walla/SKILL.md +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/skills/walla-buy/SKILL.md +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/tests/test_account.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/tests/test_account_more.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/tests/test_catch.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/tests/test_chat_open.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/tests/test_cli_json.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/tests/test_core.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/tests/test_desk.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/tests/test_edges.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/tests/test_export.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/tests/test_http_more.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/tests/test_live.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/tests/test_negotiate.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/tests/test_parsers.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/tests/test_polite.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/tests/test_regressions.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/tests/test_search_api.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/tests/test_session_store.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/tests/test_shortlist.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/tests/test_suggest_watch.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/tests/test_verdict.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/uv.lock +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/account/__init__.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/account/actions.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/account/chat.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/account/cookies.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/account/desk.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/account/desk_watch.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/account/inbox.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/account/login.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/account/offer_wire.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/account/prompt.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/account/sell_steps.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/account/sell_wire.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/account/session_store.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/cli/__init__.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/cli/banner.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/cli/catch.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/cli/cmd_account.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/cli/cmd_desk.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/cli/cmd_ops.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/cli/console.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/core/__init__.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/core/envelope.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/core/errors.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/core/exceptions.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/core/human_fix.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/core/path.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/http/__init__.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/http/headers.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/http/polite.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/hunter/__init__.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/hunter/converge.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/hunter/export.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/hunter/export_pdf.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/hunter/negotiate.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/hunter/profile_store.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/hunter/pursuits.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/hunter/stack.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/hunter/verdict.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/hunter/watches.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/mcp/__init__.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/mcp/_entry.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/mcp/server.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/models/__init__.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/models/account.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/models/listing.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/models/profile.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/search/__init__.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/walla/search/api.py +0 -0
- {walla_cli-0.1.11 → walla_cli-0.1.13}/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,5 @@ 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** leave a sticky `must_match` from a prior hunt; clear with
|
|
71
|
+
`walla setup --must ''` or expect `mandate_note` soft shortlist fallback.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: walla-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.13
|
|
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
|
|
@@ -36,7 +36,8 @@ optional.
|
|
|
36
36
|
|
|
37
37
|
After `current_step=photo`, the steps `draft` may include `category_leaf_id`
|
|
38
38
|
(title+photo hint). `GET /api/v3/suggested-item-data/{upload_id}` often returns
|
|
39
|
-
empty HTTP 200; still poll before `loading`. CLI: `walla sell … --title …
|
|
39
|
+
empty HTTP 200; still poll before `loading`. CLI: `walla sell … --title …
|
|
40
|
+
--suggest` (explicit only; draft without `--suggest` does not upload).
|
|
40
41
|
`walla desk --watch` polls the inbox until `stack.best` converges.
|
|
41
42
|
|
|
42
43
|
## Auth
|
|
@@ -80,14 +81,25 @@ Browser: `https://es.wallapop.com/app/catalog/upload/consumer-goods`.
|
|
|
80
81
|
8. Create: `POST /api/v3/items` multipart `image` + `item` (JSON string).
|
|
81
82
|
Header **`Accept: application/vnd.upload-v2+json`** (without it → HTTP 405).
|
|
82
83
|
Extra photos: `POST /api/v3/items/{id}/picture2` multipart `image` + `order`.
|
|
83
|
-
9.
|
|
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.
|
|
84
91
|
|
|
85
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.
|
|
86
95
|
|
|
87
96
|
Every CLI call (method, path, auth, body) is listed in `fixtures/wire_contracts.json`.
|
|
88
97
|
`scripts/check_code_quality.py` fails if `walla/account`, `walla/search`, or `walla/http`
|
|
89
98
|
grows a `/api/` or `/bff/` literal that the catalog does not list.
|
|
90
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).
|
|
91
103
|
|
|
92
104
|
Writes that fail closed as `error_type: unsupported` until fixtures exist.
|
|
93
105
|
|
|
@@ -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",
|
|
@@ -17,8 +17,9 @@ walla categories --find escritorio --json
|
|
|
17
17
|
walla sell ./shot1.jpg ./shot2.jpg --title "Escritorio madera" --suggest --json
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
`--suggest`
|
|
21
|
-
`
|
|
20
|
+
`--suggest` uploads photos and asks Wallapop to prefill `category_leaf_id`.
|
|
21
|
+
Draft without `--suggest` stays local (no upload). Ask remaining
|
|
22
|
+
`data.questions`, then:
|
|
22
23
|
|
|
23
24
|
```bash
|
|
24
25
|
walla sell ./shot1.jpg ./shot2.jpg \
|
|
@@ -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(), " ")
|
|
@@ -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]
|
|
@@ -70,6 +70,63 @@ def test_missing_sell_fields() -> None:
|
|
|
70
70
|
assert CONDITIONS[0] == "as_good_as_new"
|
|
71
71
|
|
|
72
72
|
|
|
73
|
+
def test_sell_draft_with_title_does_not_suggest(tmp_path: Path) -> None:
|
|
74
|
+
from typer.testing import CliRunner
|
|
75
|
+
|
|
76
|
+
from walla.cli.main import app
|
|
77
|
+
|
|
78
|
+
photo = tmp_path / "a.jpg"
|
|
79
|
+
photo.write_bytes(b"\xff\xd8\xff\xd9")
|
|
80
|
+
runner = CliRunner()
|
|
81
|
+
with (
|
|
82
|
+
patch("walla.cli.cmd_sell.load_profile") as lp,
|
|
83
|
+
patch("walla.cli.cmd_sell.suggest_from_photos") as sug,
|
|
84
|
+
):
|
|
85
|
+
profile = MagicMock()
|
|
86
|
+
profile.lat = 41.39
|
|
87
|
+
profile.lon = 2.17
|
|
88
|
+
profile.label = "BCN"
|
|
89
|
+
lp.return_value = profile
|
|
90
|
+
result = runner.invoke(
|
|
91
|
+
app, ["sell", str(photo), "--title", "Escritorio", "--json"]
|
|
92
|
+
)
|
|
93
|
+
assert result.exit_code == 0, result.output
|
|
94
|
+
sug.assert_not_called()
|
|
95
|
+
payload = json.loads(result.output)
|
|
96
|
+
assert payload["data"]["status"] == "draft"
|
|
97
|
+
assert "suggested" not in payload["data"]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_publish_prepared_reuses_upload(tmp_path: Path) -> None:
|
|
101
|
+
from walla.account.sell import publish_prepared
|
|
102
|
+
|
|
103
|
+
photo = tmp_path / "a.jpg"
|
|
104
|
+
photo.write_bytes(b"\xff\xd8\xff\xd9")
|
|
105
|
+
listing = MagicMock()
|
|
106
|
+
listing.url = "https://es.wallapop.com/item/x"
|
|
107
|
+
listing.web_slug = "x"
|
|
108
|
+
with (
|
|
109
|
+
patch(
|
|
110
|
+
"walla.account.sell.create_listing",
|
|
111
|
+
return_value={"id": "abc", "flags": {}},
|
|
112
|
+
) as create,
|
|
113
|
+
patch("walla.account.sell.get_item", return_value=listing),
|
|
114
|
+
):
|
|
115
|
+
out = publish_prepared(
|
|
116
|
+
[photo],
|
|
117
|
+
upload_id="upload-1",
|
|
118
|
+
title="T",
|
|
119
|
+
description="D",
|
|
120
|
+
price_eur=10,
|
|
121
|
+
category_leaf_id="24208",
|
|
122
|
+
lat=41.0,
|
|
123
|
+
lon=2.0,
|
|
124
|
+
)
|
|
125
|
+
assert out["reused_suggest"] is True
|
|
126
|
+
assert out["upload_id"] == "upload-1"
|
|
127
|
+
create.assert_called_once()
|
|
128
|
+
|
|
129
|
+
|
|
73
130
|
def test_sell_cmd_draft_without_yes(tmp_path: Path) -> None:
|
|
74
131
|
from typer.testing import CliRunner
|
|
75
132
|
|
|
@@ -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 = [
|
|
@@ -16,6 +16,7 @@ from walla.account.sell_steps import (
|
|
|
16
16
|
poll_suggested,
|
|
17
17
|
post_step,
|
|
18
18
|
)
|
|
19
|
+
from walla.account.sell_upload import IMAGE_TYPES, upload_pictures
|
|
19
20
|
from walla.account.sell_wire import build_item_body
|
|
20
21
|
from walla.core.exceptions import (
|
|
21
22
|
WallaHTTPError,
|
|
@@ -33,45 +34,11 @@ __all__ = (
|
|
|
33
34
|
"create_listing",
|
|
34
35
|
"delete_listing",
|
|
35
36
|
"publish_listing",
|
|
37
|
+
"publish_prepared",
|
|
36
38
|
"upload_pictures",
|
|
37
39
|
)
|
|
38
40
|
|
|
39
41
|
_UPLOAD_ACCEPT = "application/vnd.upload-v2+json"
|
|
40
|
-
_IMAGE_TYPES = {
|
|
41
|
-
".jpg": "image/jpeg",
|
|
42
|
-
".jpeg": "image/jpeg",
|
|
43
|
-
".png": "image/png",
|
|
44
|
-
".webp": "image/webp",
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def upload_pictures(upload_id: str, photos: list[Path]) -> int:
|
|
49
|
-
"""POST each photo to /api/v3/upload/{id}/pictures (HTTP 204)."""
|
|
50
|
-
if not photos:
|
|
51
|
-
raise ValueError("at least one photo required")
|
|
52
|
-
headers = {k: v for k, v in auth_headers().items() if k.lower() != "content-type"}
|
|
53
|
-
session: Any = requests.Session()
|
|
54
|
-
for path in photos:
|
|
55
|
-
data = path.read_bytes()
|
|
56
|
-
ctype = _IMAGE_TYPES.get(path.suffix.lower(), "image/jpeg")
|
|
57
|
-
wait_turn()
|
|
58
|
-
mp = CurlMime()
|
|
59
|
-
mp.addpart(name="file", content_type=ctype, filename=path.name, data=data)
|
|
60
|
-
resp: Any = session.request(
|
|
61
|
-
"POST",
|
|
62
|
-
f"{API_BASE}/api/v3/upload/{upload_id}/pictures",
|
|
63
|
-
headers=headers,
|
|
64
|
-
multipart=mp,
|
|
65
|
-
impersonate="chrome",
|
|
66
|
-
timeout=60,
|
|
67
|
-
)
|
|
68
|
-
mp.close()
|
|
69
|
-
if resp.status_code not in (200, 201, 204):
|
|
70
|
-
raise WallaHTTPError(
|
|
71
|
-
f"picture upload HTTP {resp.status_code}: {(resp.text or '')[:200]}",
|
|
72
|
-
status_code=resp.status_code,
|
|
73
|
-
)
|
|
74
|
-
return len(photos)
|
|
75
42
|
|
|
76
43
|
|
|
77
44
|
def create_listing(item: dict[str, Any], first_photo: Path) -> dict[str, Any]:
|
|
@@ -79,7 +46,7 @@ def create_listing(item: dict[str, Any], first_photo: Path) -> dict[str, Any]:
|
|
|
79
46
|
headers = {k: v for k, v in auth_headers().items() if k.lower() != "content-type"}
|
|
80
47
|
headers["Accept"] = _UPLOAD_ACCEPT
|
|
81
48
|
data = first_photo.read_bytes()
|
|
82
|
-
ctype =
|
|
49
|
+
ctype = IMAGE_TYPES.get(first_photo.suffix.lower(), "image/jpeg")
|
|
83
50
|
wait_turn()
|
|
84
51
|
mp = CurlMime()
|
|
85
52
|
mp.addpart(name="image", content_type=ctype, filename=first_photo.name, data=data)
|
|
@@ -110,6 +77,54 @@ def delete_listing(item_id: str, *, client: HttpClient | None = None) -> dict[st
|
|
|
110
77
|
return {"id": item_id, "deleted": True}
|
|
111
78
|
|
|
112
79
|
|
|
80
|
+
def publish_prepared(
|
|
81
|
+
photos: list[Path],
|
|
82
|
+
*,
|
|
83
|
+
upload_id: str,
|
|
84
|
+
title: str,
|
|
85
|
+
description: str,
|
|
86
|
+
price_eur: float,
|
|
87
|
+
category_leaf_id: str,
|
|
88
|
+
lat: float,
|
|
89
|
+
lon: float,
|
|
90
|
+
condition: str = "good",
|
|
91
|
+
shipping: bool = False,
|
|
92
|
+
weight_kg: float | None = None,
|
|
93
|
+
wire_draft: dict[str, str] | None = None,
|
|
94
|
+
) -> dict[str, Any]:
|
|
95
|
+
"""Create item from an upload_id already walked through loading."""
|
|
96
|
+
paths = [Path(p) for p in photos]
|
|
97
|
+
for p in paths:
|
|
98
|
+
if not p.is_file():
|
|
99
|
+
raise ValueError(f"photo not found: {p}")
|
|
100
|
+
if not upload_id.strip():
|
|
101
|
+
raise ValueError("upload_id required")
|
|
102
|
+
item = build_item_body(
|
|
103
|
+
upload_id=upload_id,
|
|
104
|
+
title=title,
|
|
105
|
+
description=description,
|
|
106
|
+
price_eur=price_eur,
|
|
107
|
+
category_leaf_id=category_leaf_id,
|
|
108
|
+
lat=lat,
|
|
109
|
+
lon=lon,
|
|
110
|
+
condition=condition,
|
|
111
|
+
shipping=shipping,
|
|
112
|
+
max_weight_kg=weight_kg,
|
|
113
|
+
)
|
|
114
|
+
created = create_listing(item, paths[0])
|
|
115
|
+
if len(paths) > 1:
|
|
116
|
+
_upload_extra_pictures(created["id"], paths[1:])
|
|
117
|
+
return {
|
|
118
|
+
**created,
|
|
119
|
+
"upload_id": upload_id,
|
|
120
|
+
"title": title.strip()[:50],
|
|
121
|
+
"url": _public_item_url(str(created["id"])),
|
|
122
|
+
"photos": len(paths),
|
|
123
|
+
"wire_draft": wire_draft or {},
|
|
124
|
+
"reused_suggest": True,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
|
|
113
128
|
def publish_listing(
|
|
114
129
|
photos: list[Path],
|
|
115
130
|
*,
|
|
@@ -143,7 +158,8 @@ def publish_listing(
|
|
|
143
158
|
post_step(http, upload_id, current="category", draft=draft)
|
|
144
159
|
poll_suggested(http, upload_id)
|
|
145
160
|
post_step(http, upload_id, current="loading", draft=draft)
|
|
146
|
-
|
|
161
|
+
out = publish_prepared(
|
|
162
|
+
paths,
|
|
147
163
|
upload_id=upload_id,
|
|
148
164
|
title=title,
|
|
149
165
|
description=description,
|
|
@@ -153,19 +169,11 @@ def publish_listing(
|
|
|
153
169
|
lon=lon,
|
|
154
170
|
condition=condition,
|
|
155
171
|
shipping=shipping,
|
|
156
|
-
|
|
172
|
+
weight_kg=weight_kg,
|
|
173
|
+
wire_draft=wire,
|
|
157
174
|
)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
_upload_extra_pictures(created["id"], paths[1:])
|
|
161
|
-
return {
|
|
162
|
-
**created,
|
|
163
|
-
"upload_id": upload_id,
|
|
164
|
-
"title": title.strip()[:50],
|
|
165
|
-
"url": _public_item_url(str(created["id"])),
|
|
166
|
-
"photos": len(paths),
|
|
167
|
-
"wire_draft": wire,
|
|
168
|
-
}
|
|
175
|
+
out["reused_suggest"] = False
|
|
176
|
+
return out
|
|
169
177
|
|
|
170
178
|
|
|
171
179
|
def _public_item_url(item_id: str) -> str:
|
|
@@ -194,7 +202,7 @@ def _upload_extra_pictures(item_id: str, photos: list[Path]) -> None:
|
|
|
194
202
|
session: Any = requests.Session()
|
|
195
203
|
for idx, path in enumerate(photos, start=1):
|
|
196
204
|
data = path.read_bytes()
|
|
197
|
-
ctype =
|
|
205
|
+
ctype = IMAGE_TYPES.get(path.suffix.lower(), "image/jpeg")
|
|
198
206
|
wait_turn()
|
|
199
207
|
mp = CurlMime()
|
|
200
208
|
mp.addpart(name="image", content_type=ctype, filename=path.name, data=data)
|