walla-cli 0.1.12__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.12 → walla_cli-0.1.13}/AGENTS.md +5 -1
- {walla_cli-0.1.12 → walla_cli-0.1.13}/PKG-INFO +1 -1
- {walla_cli-0.1.12 → walla_cli-0.1.13}/docs/WIRE.md +12 -1
- {walla_cli-0.1.12 → 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.12 → walla_cli-0.1.13}/fixtures/wire_contracts.json +13 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/pyproject.toml +1 -1
- {walla_cli-0.1.12 → 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.12 → walla_cli-0.1.13}/tests/test_wire_contracts.py +18 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/__init__.py +1 -1
- 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.13/walla/cli/cmd_edit.py +89 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/cli/cmd_look.py +10 -2
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/cli/cmd_sell.py +3 -5
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/cli/main.py +2 -1
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/core/instruct.py +7 -3
- {walla_cli-0.1.12 → 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.12 → walla_cli-0.1.13}/walla/hunter/shortlist.py +35 -3
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/search/pick_category.py +22 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/.cursor/rules/quality.mdc +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/.github/workflows/ci.yml +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/.gitignore +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/LICENSE +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/README.md +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/docs/assets/walla-demo-poster.jpg +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/docs/assets/walla-demo-preview.gif +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/docs/assets/walla-demo.mp4 +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/docs/assets/walla-github-banner.jpg +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/docs/code_quality.md +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/docs/data_engineering_standards.md +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/docs/mcp.md +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/docs/objective_function.md +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/docs/rate_limit_analysis.md +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/fixtures/item_detail.json +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/fixtures/offer_buyer_request.json +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/fixtures/search_bicicleta.json +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/fixtures/sell_item_request.json +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/fixtures/sell_suggest_draft.json +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/scripts/check_code_quality.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/scripts/dogfood_run.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/scripts/probe_rate.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/skills/walla/SKILL.md +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/skills/walla-buy/SKILL.md +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/skills/walla-sell/SKILL.md +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/tests/test_account.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/tests/test_account_more.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/tests/test_catch.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/tests/test_chat_open.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/tests/test_cli_json.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/tests/test_core.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/tests/test_desk.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/tests/test_edges.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/tests/test_export.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/tests/test_http_more.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/tests/test_live.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/tests/test_negotiate.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/tests/test_parsers.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/tests/test_polite.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/tests/test_regressions.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/tests/test_search_api.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/tests/test_sell.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/tests/test_session_store.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/tests/test_shortlist.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/tests/test_suggest_watch.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/tests/test_verdict.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/uv.lock +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/account/__init__.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/account/actions.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/account/chat.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/account/cookies.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/account/desk.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/account/desk_watch.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/account/inbox.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/account/login.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/account/offer_wire.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/account/prompt.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/account/sell.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/account/sell_steps.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/account/sell_suggest.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/account/sell_upload.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/account/sell_wire.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/account/session_store.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/cli/__init__.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/cli/banner.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/cli/catch.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/cli/cmd_account.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/cli/cmd_desk.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/cli/cmd_ops.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/cli/console.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/core/__init__.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/core/envelope.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/core/errors.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/core/exceptions.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/core/human_fix.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/core/path.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/http/__init__.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/http/headers.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/http/polite.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/hunter/__init__.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/hunter/converge.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/hunter/export.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/hunter/export_pdf.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/hunter/negotiate.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/hunter/profile_store.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/hunter/pursuits.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/hunter/stack.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/hunter/verdict.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/hunter/watches.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/mcp/__init__.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/mcp/_entry.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/mcp/server.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/models/__init__.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/models/account.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/models/listing.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/models/profile.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/search/__init__.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/search/api.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.13}/walla/search/category_find.py +0 -0
- {walla_cli-0.1.12 → 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
|
|
@@ -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.
|
|
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
|
|
|
@@ -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",
|
|
@@ -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]
|
|
@@ -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 = [
|
|
@@ -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
|
+
)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Edit-item body (fixture-backed PUT /api/v3/items/{id})."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from walla.account.sell_wire import CONDITIONS
|
|
8
|
+
|
|
9
|
+
__all__ = (
|
|
10
|
+
"EDIT_ATTR_KEYS",
|
|
11
|
+
"EDIT_ITEM_KEYS",
|
|
12
|
+
"assert_edit_body",
|
|
13
|
+
"build_edit_body",
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
EDIT_ITEM_KEYS = (
|
|
17
|
+
"attributes",
|
|
18
|
+
"category_leaf_id",
|
|
19
|
+
"apply_discount",
|
|
20
|
+
"delivery",
|
|
21
|
+
"location",
|
|
22
|
+
"pictures",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
EDIT_ATTR_KEYS = ("title", "description", "price_amount", "condition")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def build_edit_body(
|
|
29
|
+
*,
|
|
30
|
+
title: str,
|
|
31
|
+
description: str,
|
|
32
|
+
price_eur: float,
|
|
33
|
+
category_leaf_id: str,
|
|
34
|
+
condition: str,
|
|
35
|
+
lat: float,
|
|
36
|
+
lon: float,
|
|
37
|
+
pictures: list[dict[str, Any]],
|
|
38
|
+
shipping: bool = False,
|
|
39
|
+
max_weight_kg: float | None = None,
|
|
40
|
+
approximated: bool = False,
|
|
41
|
+
) -> dict[str, Any]:
|
|
42
|
+
if condition not in CONDITIONS:
|
|
43
|
+
raise ValueError(f"condition must be one of {CONDITIONS}")
|
|
44
|
+
if not pictures:
|
|
45
|
+
raise ValueError("edit body needs at least one picture id")
|
|
46
|
+
body: dict[str, Any] = {
|
|
47
|
+
"attributes": {
|
|
48
|
+
"title": title.strip()[:50],
|
|
49
|
+
"description": description.strip(),
|
|
50
|
+
"price_amount": float(price_eur),
|
|
51
|
+
"condition": condition,
|
|
52
|
+
},
|
|
53
|
+
"category_leaf_id": str(category_leaf_id),
|
|
54
|
+
"apply_discount": False,
|
|
55
|
+
"delivery": {
|
|
56
|
+
"allowed_by_user": bool(shipping),
|
|
57
|
+
"max_weight_kg": (
|
|
58
|
+
float(max_weight_kg) if shipping and max_weight_kg else None
|
|
59
|
+
),
|
|
60
|
+
"cost_configuration_id": None,
|
|
61
|
+
},
|
|
62
|
+
"location": {
|
|
63
|
+
"latitude": float(lat),
|
|
64
|
+
"longitude": float(lon),
|
|
65
|
+
"approximated": bool(approximated),
|
|
66
|
+
},
|
|
67
|
+
"pictures": pictures,
|
|
68
|
+
}
|
|
69
|
+
assert_edit_body(body)
|
|
70
|
+
return body
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def assert_edit_body(body: dict[str, Any]) -> None:
|
|
74
|
+
missing = [k for k in EDIT_ITEM_KEYS if k not in body]
|
|
75
|
+
if missing:
|
|
76
|
+
raise ValueError(f"edit item body missing keys: {missing}")
|
|
77
|
+
attrs = body["attributes"]
|
|
78
|
+
if not isinstance(attrs, dict):
|
|
79
|
+
raise ValueError("edit item attributes must be an object")
|
|
80
|
+
for k in EDIT_ATTR_KEYS:
|
|
81
|
+
if k not in attrs:
|
|
82
|
+
raise ValueError(f"edit item attributes missing {k}")
|
|
83
|
+
pics = body["pictures"]
|
|
84
|
+
if not isinstance(pics, list) or not pics:
|
|
85
|
+
raise ValueError("edit pictures must be a non-empty list")
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Normalize sell photo paths (HEIC -> JPEG via ImageMagick when needed)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from walla.account.sell_upload import IMAGE_TYPES
|
|
10
|
+
|
|
11
|
+
__all__ = ("prepare_sell_photos",)
|
|
12
|
+
|
|
13
|
+
_HEIC = {".heic", ".heif"}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def prepare_sell_photos(photos: list[Path]) -> list[Path]:
|
|
17
|
+
"""Return paths Wallapop accepts; convert HEIC/HEIF when convert/magick exists."""
|
|
18
|
+
if not photos:
|
|
19
|
+
raise ValueError("at least one photo required")
|
|
20
|
+
out: list[Path] = []
|
|
21
|
+
for path in photos:
|
|
22
|
+
p = path.expanduser()
|
|
23
|
+
if not p.is_file():
|
|
24
|
+
raise ValueError(f"photo not found: {p}")
|
|
25
|
+
suf = p.suffix.lower()
|
|
26
|
+
if suf in IMAGE_TYPES:
|
|
27
|
+
out.append(p)
|
|
28
|
+
continue
|
|
29
|
+
if suf in _HEIC:
|
|
30
|
+
out.append(_heic_to_jpeg(p))
|
|
31
|
+
continue
|
|
32
|
+
raise ValueError(
|
|
33
|
+
f"unsupported photo type {suf} ({p.name}); use JPEG/PNG/WebP/HEIC"
|
|
34
|
+
)
|
|
35
|
+
return out
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _heic_to_jpeg(src: Path) -> Path:
|
|
39
|
+
dest = src.with_suffix(".jpg")
|
|
40
|
+
if dest.is_file() and dest.stat().st_mtime >= src.stat().st_mtime:
|
|
41
|
+
return dest
|
|
42
|
+
tool = shutil.which("magick") or shutil.which("convert")
|
|
43
|
+
if not tool:
|
|
44
|
+
raise ValueError(
|
|
45
|
+
f"HEIC needs ImageMagick (magick/convert) to encode JPEG: {src.name}"
|
|
46
|
+
)
|
|
47
|
+
cmd = (
|
|
48
|
+
[tool, str(src), str(dest)]
|
|
49
|
+
if Path(tool).name == "convert"
|
|
50
|
+
else [tool, str(src), str(dest)]
|
|
51
|
+
)
|
|
52
|
+
proc = subprocess.run(cmd, capture_output=True, text=True, check=False)
|
|
53
|
+
if proc.returncode != 0 or not dest.is_file():
|
|
54
|
+
err = (proc.stderr or proc.stdout or "").strip()[:200]
|
|
55
|
+
raise ValueError(f"HEIC convert failed for {src.name}: {err or 'unknown'}")
|
|
56
|
+
return dest
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Edit owned listings (price / description / shipping)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from walla.account.sell_edit import edit_listing
|
|
10
|
+
from walla.cli.catch import run_cmd
|
|
11
|
+
from walla.hunter.pursuits import Pursuit, load_pursuits, remember
|
|
12
|
+
|
|
13
|
+
__all__ = ("register",)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def register(app: typer.Typer) -> None:
|
|
17
|
+
@app.command("edit")
|
|
18
|
+
def edit_cmd(
|
|
19
|
+
item_id: str = typer.Argument(..., help="Owned listing id"),
|
|
20
|
+
title: str | None = typer.Option(None, "--title"),
|
|
21
|
+
description: str | None = typer.Option(None, "--description", "--desc"),
|
|
22
|
+
eur: float | None = typer.Option(None, "--eur", help="New asking price"),
|
|
23
|
+
ship: bool | None = typer.Option(
|
|
24
|
+
None, "--ship/--no-ship", help="Toggle Wallapop shipping"
|
|
25
|
+
),
|
|
26
|
+
weight_kg: float | None = typer.Option(None, "--weight-kg"),
|
|
27
|
+
floor: float | None = typer.Option(
|
|
28
|
+
None, "--floor", help="Update desk walk-away EUR for this listing"
|
|
29
|
+
),
|
|
30
|
+
yes: bool = typer.Option(False, "--yes", help="Apply edit for real"),
|
|
31
|
+
json: bool = typer.Option(False, "--json"),
|
|
32
|
+
) -> None:
|
|
33
|
+
"""Update title, description, price, or shipping on an owned listing."""
|
|
34
|
+
|
|
35
|
+
def _run() -> dict[str, Any]:
|
|
36
|
+
if title is None and description is None and eur is None and ship is None:
|
|
37
|
+
raise ValueError("Pass --title, --desc, --eur, and/or --ship")
|
|
38
|
+
if not yes:
|
|
39
|
+
return {
|
|
40
|
+
"status": "draft",
|
|
41
|
+
"id": item_id,
|
|
42
|
+
"fields": {
|
|
43
|
+
"title": title,
|
|
44
|
+
"description": description,
|
|
45
|
+
"eur": eur,
|
|
46
|
+
"shipping": ship,
|
|
47
|
+
"weight_kg": weight_kg,
|
|
48
|
+
"floor": floor,
|
|
49
|
+
},
|
|
50
|
+
"hint": "Review, then walla edit <id> … --yes",
|
|
51
|
+
}
|
|
52
|
+
edited = edit_listing(
|
|
53
|
+
item_id,
|
|
54
|
+
title=title,
|
|
55
|
+
description=description,
|
|
56
|
+
price_eur=eur,
|
|
57
|
+
shipping=ship,
|
|
58
|
+
weight_kg=weight_kg,
|
|
59
|
+
)
|
|
60
|
+
if floor is not None or eur is not None:
|
|
61
|
+
_touch_floor(item_id, ask=eur, floor=floor)
|
|
62
|
+
return {"status": "edited", **edited}
|
|
63
|
+
|
|
64
|
+
run_cmd(_run, as_json=json)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _touch_floor(item_id: str, *, ask: float | None, floor: float | None) -> None:
|
|
68
|
+
for row in load_pursuits().pursuits:
|
|
69
|
+
if row.item_id != item_id or row.side != "sell":
|
|
70
|
+
continue
|
|
71
|
+
ask_eur = float(ask) if ask is not None else row.ask_eur
|
|
72
|
+
walk = float(floor) if floor is not None else row.walk_away_eur
|
|
73
|
+
if ask is not None and floor is None:
|
|
74
|
+
walk = min(walk, ask_eur)
|
|
75
|
+
remember(
|
|
76
|
+
Pursuit(
|
|
77
|
+
item_id=row.item_id,
|
|
78
|
+
title=row.title,
|
|
79
|
+
url=row.url,
|
|
80
|
+
side="sell",
|
|
81
|
+
ask_eur=ask_eur,
|
|
82
|
+
offer_eur=ask_eur,
|
|
83
|
+
walk_away_eur=walk,
|
|
84
|
+
query=row.query,
|
|
85
|
+
status=row.status,
|
|
86
|
+
why=row.why,
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
return
|