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.
- {walla_cli-0.1.12 → walla_cli-0.1.14}/AGENTS.md +8 -1
- {walla_cli-0.1.12 → walla_cli-0.1.14}/PKG-INFO +16 -2
- {walla_cli-0.1.12 → walla_cli-0.1.14}/README.md +15 -1
- {walla_cli-0.1.12 → walla_cli-0.1.14}/docs/WIRE.md +12 -1
- {walla_cli-0.1.12 → walla_cli-0.1.14}/docs/data_engineering_standards.md +3 -1
- {walla_cli-0.1.12 → walla_cli-0.1.14}/fixtures/categories.json +36 -0
- walla_cli-0.1.14/fixtures/edit_item_request.json +49 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/fixtures/wire_contracts.json +13 -0
- walla_cli-0.1.14/incoming/teach/README.md +3 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/pyproject.toml +1 -1
- {walla_cli-0.1.12 → walla_cli-0.1.14}/tests/test_categories_find.py +11 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/tests/test_cli_json.py +5 -1
- {walla_cli-0.1.12 → walla_cli-0.1.14}/tests/test_core.py +2 -1
- walla_cli-0.1.14/tests/test_edit.py +94 -0
- walla_cli-0.1.14/tests/test_teach.py +73 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/tests/test_wire_contracts.py +18 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/__init__.py +1 -1
- walla_cli-0.1.14/walla/account/sell_edit.py +106 -0
- walla_cli-0.1.14/walla/account/sell_edit_wire.py +85 -0
- walla_cli-0.1.14/walla/account/sell_photos.py +56 -0
- walla_cli-0.1.14/walla/cli/cmd_edit.py +89 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/cli/cmd_look.py +10 -2
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/cli/cmd_ops.py +6 -1
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/cli/cmd_sell.py +3 -5
- walla_cli-0.1.14/walla/cli/cmd_teach.py +53 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/cli/main.py +11 -1
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/core/instruct.py +20 -3
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/core/path.py +10 -0
- walla_cli-0.1.14/walla/core/teach.py +138 -0
- walla_cli-0.1.14/walla/core/teach_pr.py +199 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/http/client.py +6 -0
- walla_cli-0.1.14/walla/http/sign.py +32 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/hunter/shortlist.py +35 -3
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/mcp/server.py +22 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/search/pick_category.py +22 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/.cursor/rules/quality.mdc +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/.github/workflows/ci.yml +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/.gitignore +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/LICENSE +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/docs/assets/walla-demo-poster.jpg +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/docs/assets/walla-demo-preview.gif +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/docs/assets/walla-demo.mp4 +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/docs/assets/walla-github-banner.jpg +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/docs/code_quality.md +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/docs/mcp.md +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/docs/objective_function.md +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/docs/rate_limit_analysis.md +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/fixtures/item_detail.json +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/fixtures/offer_buyer_request.json +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/fixtures/search_bicicleta.json +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/fixtures/sell_item_request.json +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/fixtures/sell_suggest_draft.json +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/scripts/check_code_quality.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/scripts/dogfood_run.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/scripts/probe_rate.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/skills/walla/SKILL.md +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/skills/walla-buy/SKILL.md +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/skills/walla-sell/SKILL.md +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/tests/test_account.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/tests/test_account_more.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/tests/test_catch.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/tests/test_chat_open.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/tests/test_desk.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/tests/test_edges.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/tests/test_export.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/tests/test_http_more.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/tests/test_live.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/tests/test_negotiate.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/tests/test_parsers.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/tests/test_polite.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/tests/test_regressions.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/tests/test_search_api.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/tests/test_sell.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/tests/test_session_store.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/tests/test_shortlist.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/tests/test_suggest_watch.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/tests/test_verdict.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/uv.lock +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/account/__init__.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/account/actions.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/account/chat.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/account/cookies.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/account/desk.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/account/desk_watch.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/account/inbox.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/account/login.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/account/offer_wire.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/account/prompt.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/account/sell.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/account/sell_steps.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/account/sell_suggest.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/account/sell_upload.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/account/sell_wire.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/account/session_store.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/cli/__init__.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/cli/banner.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/cli/catch.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/cli/cmd_account.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/cli/cmd_desk.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/cli/console.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/core/__init__.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/core/envelope.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/core/errors.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/core/exceptions.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/core/human_fix.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/http/__init__.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/http/headers.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/http/polite.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/hunter/__init__.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/hunter/converge.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/hunter/export.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/hunter/export_pdf.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/hunter/negotiate.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/hunter/profile_store.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/hunter/pursuits.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/hunter/stack.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/hunter/verdict.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/hunter/watches.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/mcp/__init__.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/mcp/_entry.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/models/__init__.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/models/account.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/models/listing.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/models/profile.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/search/__init__.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/search/api.py +0 -0
- {walla_cli-0.1.12 → walla_cli-0.1.14}/walla/search/category_find.py +0 -0
- {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.
|
|
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
|
|
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
|
|
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.
|
|
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",
|
|
@@ -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 = [
|
|
@@ -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
|
+
)
|