scrapebadger-cli 0.23.2__tar.gz → 0.24.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. scrapebadger_cli-0.24.0/PKG-INFO +171 -0
  2. scrapebadger_cli-0.24.0/README.md +149 -0
  3. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/pyproject.toml +1 -1
  4. scrapebadger_cli-0.24.0/src/scrapebadger_cli/client.py +114 -0
  5. scrapebadger_cli-0.24.0/src/scrapebadger_cli/commands/agent.py +242 -0
  6. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/auth.py +1 -1
  7. scrapebadger_cli-0.24.0/src/scrapebadger_cli/commands/init.py +163 -0
  8. scrapebadger_cli-0.24.0/src/scrapebadger_cli/commands/onboarding.py +182 -0
  9. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/config.py +24 -5
  10. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/main.py +26 -0
  11. scrapebadger_cli-0.24.0/src/scrapebadger_cli/results.py +76 -0
  12. scrapebadger_cli-0.24.0/src/scrapebadger_cli/views.py +109 -0
  13. scrapebadger_cli-0.24.0/tests/test_agent_features.py +350 -0
  14. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/uv.lock +1 -1
  15. scrapebadger_cli-0.23.2/PKG-INFO +0 -97
  16. scrapebadger_cli-0.23.2/README.md +0 -75
  17. scrapebadger_cli-0.23.2/src/scrapebadger_cli/client.py +0 -70
  18. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/.gitignore +0 -0
  19. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/__init__.py +0 -0
  20. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/__init__.py +0 -0
  21. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/airbnb.py +0 -0
  22. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/aliexpress.py +0 -0
  23. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/amazon.py +0 -0
  24. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/apartments.py +0 -0
  25. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/app_store.py +0 -0
  26. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/asos.py +0 -0
  27. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/baidu.py +0 -0
  28. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/bestbuy.py +0 -0
  29. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/bing.py +0 -0
  30. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/booking.py +0 -0
  31. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/chatgpt.py +0 -0
  32. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/depop.py +0 -0
  33. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/duckduckgo.py +0 -0
  34. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/ebay.py +0 -0
  35. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/etsy.py +0 -0
  36. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/facebook.py +0 -0
  37. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/gemini.py +0 -0
  38. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/google.py +0 -0
  39. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/google_play.py +0 -0
  40. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/gumtree.py +0 -0
  41. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/idealista.py +0 -0
  42. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/immobiliare.py +0 -0
  43. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/instagram.py +0 -0
  44. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/leboncoin.py +0 -0
  45. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/linkedin.py +0 -0
  46. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/loopnet.py +0 -0
  47. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/naver.py +0 -0
  48. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/perplexity.py +0 -0
  49. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/realtor.py +0 -0
  50. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/reddit.py +0 -0
  51. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/redfin.py +0 -0
  52. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/stream.py +0 -0
  53. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/target.py +0 -0
  54. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/tiktok.py +0 -0
  55. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/twitter.py +0 -0
  56. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/vinted.py +0 -0
  57. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/wallapop.py +0 -0
  58. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/walmart.py +0 -0
  59. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/web.py +0 -0
  60. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/yahoo.py +0 -0
  61. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/yandex.py +0 -0
  62. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/youtube.py +0 -0
  63. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/zillow.py +0 -0
  64. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/output.py +0 -0
  65. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/tests/test_google_light_search.py +0 -0
  66. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/tests/test_naver.py +0 -0
  67. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/tests/test_tiktok_pagination.py +0 -0
  68. {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/tests/test_vinted_image_search.py +0 -0
@@ -0,0 +1,171 @@
1
+ Metadata-Version: 2.5
2
+ Name: scrapebadger-cli
3
+ Version: 0.24.0
4
+ Summary: ScrapeBadger CLI — Twitter, Vinted, Google, and Web Scraping from the command line
5
+ Author: ScrapeBadger Team
6
+ License: MIT
7
+ Keywords: ai-tools,cli,mcp,scrapebadger,twitter,vinted,web-scraping
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Topic :: Internet :: WWW/HTTP
12
+ Classifier: Topic :: Software Development :: Libraries
13
+ Requires-Python: >=3.10
14
+ Requires-Dist: httpx>=0.26.0
15
+ Requires-Dist: rich>=13.0.0
16
+ Requires-Dist: typer>=0.12.0
17
+ Requires-Dist: websockets>=12.0
18
+ Provides-Extra: dev
19
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
20
+ Requires-Dist: ruff>=0.2.0; extra == 'dev'
21
+ Description-Content-Type: text/markdown
22
+
23
+ # ScrapeBadger CLI
24
+
25
+ Google Light Search uses the dedicated `/v1/google/light/search` endpoint:
26
+
27
+ ```bash
28
+ scrapebadger google light-search "vinted api" --gl us --hl en
29
+ ```
30
+
31
+ Light returns organic results and related searches for 10 credits per successful
32
+ uncached request. Regular `scrapebadger google search` costs 20 credits and
33
+ returns the full SERP. Cache hits and failed requests are free.
34
+
35
+ Command-line access to ScrapeBadger APIs, including Etsy, AliExpress, Target, ASOS, Best Buy, search engines, social media and general web scraping.
36
+
37
+ ## Install
38
+
39
+ ```bash
40
+ pip install scrapebadger-cli
41
+ # or: uv tool install scrapebadger-cli
42
+ ```
43
+
44
+ The installed command is `scrapebadger`. To run without installing:
45
+
46
+ ```bash
47
+ uvx --from scrapebadger-cli scrapebadger --help
48
+ ```
49
+
50
+ ## Quick Start
51
+
52
+ ```bash
53
+ scrapebadger login # approve in the browser; or: scrapebadger login --api-key sb_live_...
54
+ scrapebadger twitter user elonmusk
55
+ scrapebadger twitter search "AI agents" --output table
56
+ scrapebadger vinted search "nike shoes" --market de
57
+ scrapebadger web scrape https://example.com --js
58
+ ```
59
+
60
+ ## Getting a key and setting up agents (0.24.0+)
61
+
62
+ ```bash
63
+ scrapebadger init # detect Claude Code / Codex / Cursor; install skill + MCP
64
+ scrapebadger init --yes --all # same, without prompts
65
+ scrapebadger login # device flow: open the link, approve, key is saved
66
+ scrapebadger trial # 50 free credits without an account (one per network, 7 days)
67
+ scrapebadger connect # print the link that attaches a trial key to an account
68
+ ```
69
+
70
+ `init` installs the ScrapeBadger skill (`SKILL.md` and `references/endpoints.md`)
71
+ into `~/.claude/skills/scrapebadger` and `~/.codex/skills/scrapebadger`, registers
72
+ the hosted MCP server (`claude mcp add --scope user ...`, `codex mcp add ...`, or a
73
+ merge into `~/.cursor/mcp.json` that keeps your other servers), and offers `login`
74
+ or `trial` when no key is configured. Cursor has no on-disk global rules location,
75
+ so it gets the MCP server only.
76
+
77
+ `trial` solves a short proof of work, saves the key and prints a notice to pass on
78
+ to your human. A trial key works only from the IP that created it, on read APIs,
79
+ at 10 requests/min, and not through the hosted MCP server. It refuses to replace an
80
+ account key unless you pass `--force`.
81
+
82
+ ## Saved results and free re-reads
83
+
84
+ Every API command also writes its full JSON response to
85
+ `~/.cache/scrapebadger/results/<api>-<command>-<UTC timestamp>.json` (honours
86
+ `XDG_CACHE_HOME`; the newest 200 are kept) and prints one line to stderr:
87
+
88
+ ```
89
+ saved: ~/.cache/scrapebadger/results/google-search-20261023T101500123456Z.json · credits: 20 · result_id: 6f1c...
90
+ ```
91
+
92
+ stdout is unchanged. Disable with `scrapebadger --no-save ...` or `SCRAPEBADGER_SAVE=0`.
93
+
94
+ Re-slice a saved result locally, for free:
95
+
96
+ ```bash
97
+ scrapebadger view --summary # newest result: types, list lengths, byte sizes
98
+ scrapebadger view --last google --max-items 5 --fields title,link
99
+ scrapebadger view path/to/result.json --jq '.organic_results[].link' # needs jq on PATH
100
+ ```
101
+
102
+ `--fields` (dotted paths) and `--max-items` apply to the main item list (the
103
+ top-level array, else the largest top-level list); truncation adds
104
+ `"_truncated": {"path", "kept", "total"}`.
105
+
106
+ Re-read a result from the server within ~15 minutes, free, sliced server-side:
107
+
108
+ ```bash
109
+ scrapebadger results get <result_id> --max-items 3 --fields id,text
110
+ ```
111
+
112
+ ## Quotes, bug reports and feedback (all free)
113
+
114
+ ```bash
115
+ scrapebadger quote /v1/google/search -p q="coffee" -p gl=us # cost, validation, latency
116
+ scrapebadger quote /v1/web/scrape --method POST --body '{"url": "https://example.com"}'
117
+ scrapebadger report-bug "Empty results for a public profile" --request-id <X-Request-ID> --endpoint /v1/instagram/users/x
118
+ scrapebadger feedback "Please add Pinterest" --details "..."
119
+ ```
120
+
121
+ Error messages include the `request_id` to quote in a bug report, and the
122
+ upgrade or claim link when you are out of credits.
123
+
124
+ ## Environment
125
+
126
+ | Variable | Default | Purpose |
127
+ | --- | --- | --- |
128
+ | `SCRAPEBADGER_API_KEY` | | API key (wins over `.env` and `~/.config/scrapebadger/config.json`) |
129
+ | `SCRAPEBADGER_API_URL` | `https://scrapebadger.com` | API base URL, e.g. a local stack |
130
+ | `SCRAPEBADGER_SITE_URL` | `https://scrapebadger.com` | Where `init` downloads the skill from |
131
+ | `SCRAPEBADGER_MCP_URL` | `https://mcp.scrapebadger.com/mcp` | MCP server `init` registers |
132
+ | `SCRAPEBADGER_SAVE` | `1` | `0` disables saving results |
133
+
134
+ ## Vinted mobile data (0.14.0+)
135
+
136
+ ```sh
137
+ scrapebadger vinted mobile-operations
138
+ scrapebadger vinted mobile sold-comparables --market fr --params '{"catalog_id":"1242","brand_id":"53","status_id":"2"}'
139
+ scrapebadger vinted search "nike" --sizes 206 --materials 43
140
+ ```
141
+
142
+ The catalog lists 76 public read operations with parameter types and examples.
143
+ Each mobile read costs 1 credit; discovery is free. Sold comparables are a
144
+ Vinted-selected sample, not a complete sales history or guaranteed final sale prices.
145
+
146
+ ### Vinted image search
147
+
148
+ ```bash
149
+ scrapebadger vinted search-by-image --image photo.jpg --market nl --price-to 50
150
+ scrapebadger vinted search-by-image --image-url https://example.com/photo.webp --brand-ids 53
151
+ ```
152
+
153
+ Provide exactly one of `--image`, `--image-url` or `--image-base64`. JPEG, PNG and
154
+ WebP up to 5 MiB are supported. Returns active listings in visual ranking order;
155
+ 10 credits per successful request. Filter with `--catalog-ids`, `--brand-ids`,
156
+ `--status-ids`, `--color-ids`, `--size-ids`, `--material-ids` and price bounds.
157
+ Resend the same image and `--time` from pagination when requesting `--page 2`.
158
+
159
+ ## Dedicated ecommerce APIs
160
+
161
+ ```bash
162
+ scrapebadger etsy search "ceramic mug" --currency USD --country US
163
+ scrapebadger aliexpress search "keyboard"
164
+ scrapebadger target search "laptop" --zip-code 55403 --store-id 1375
165
+ scrapebadger asos search "shoes" --market uk
166
+ scrapebadger bestbuy search "laptop"
167
+ ```
168
+
169
+ Run `scrapebadger <api> --help` to list product, review, store, availability and
170
+ reference commands for that API. Each command supports `--help` for its exact
171
+ parameters. Requests use your API key's existing permissions and credit balance.
@@ -0,0 +1,149 @@
1
+ # ScrapeBadger CLI
2
+
3
+ Google Light Search uses the dedicated `/v1/google/light/search` endpoint:
4
+
5
+ ```bash
6
+ scrapebadger google light-search "vinted api" --gl us --hl en
7
+ ```
8
+
9
+ Light returns organic results and related searches for 10 credits per successful
10
+ uncached request. Regular `scrapebadger google search` costs 20 credits and
11
+ returns the full SERP. Cache hits and failed requests are free.
12
+
13
+ Command-line access to ScrapeBadger APIs, including Etsy, AliExpress, Target, ASOS, Best Buy, search engines, social media and general web scraping.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ pip install scrapebadger-cli
19
+ # or: uv tool install scrapebadger-cli
20
+ ```
21
+
22
+ The installed command is `scrapebadger`. To run without installing:
23
+
24
+ ```bash
25
+ uvx --from scrapebadger-cli scrapebadger --help
26
+ ```
27
+
28
+ ## Quick Start
29
+
30
+ ```bash
31
+ scrapebadger login # approve in the browser; or: scrapebadger login --api-key sb_live_...
32
+ scrapebadger twitter user elonmusk
33
+ scrapebadger twitter search "AI agents" --output table
34
+ scrapebadger vinted search "nike shoes" --market de
35
+ scrapebadger web scrape https://example.com --js
36
+ ```
37
+
38
+ ## Getting a key and setting up agents (0.24.0+)
39
+
40
+ ```bash
41
+ scrapebadger init # detect Claude Code / Codex / Cursor; install skill + MCP
42
+ scrapebadger init --yes --all # same, without prompts
43
+ scrapebadger login # device flow: open the link, approve, key is saved
44
+ scrapebadger trial # 50 free credits without an account (one per network, 7 days)
45
+ scrapebadger connect # print the link that attaches a trial key to an account
46
+ ```
47
+
48
+ `init` installs the ScrapeBadger skill (`SKILL.md` and `references/endpoints.md`)
49
+ into `~/.claude/skills/scrapebadger` and `~/.codex/skills/scrapebadger`, registers
50
+ the hosted MCP server (`claude mcp add --scope user ...`, `codex mcp add ...`, or a
51
+ merge into `~/.cursor/mcp.json` that keeps your other servers), and offers `login`
52
+ or `trial` when no key is configured. Cursor has no on-disk global rules location,
53
+ so it gets the MCP server only.
54
+
55
+ `trial` solves a short proof of work, saves the key and prints a notice to pass on
56
+ to your human. A trial key works only from the IP that created it, on read APIs,
57
+ at 10 requests/min, and not through the hosted MCP server. It refuses to replace an
58
+ account key unless you pass `--force`.
59
+
60
+ ## Saved results and free re-reads
61
+
62
+ Every API command also writes its full JSON response to
63
+ `~/.cache/scrapebadger/results/<api>-<command>-<UTC timestamp>.json` (honours
64
+ `XDG_CACHE_HOME`; the newest 200 are kept) and prints one line to stderr:
65
+
66
+ ```
67
+ saved: ~/.cache/scrapebadger/results/google-search-20261023T101500123456Z.json · credits: 20 · result_id: 6f1c...
68
+ ```
69
+
70
+ stdout is unchanged. Disable with `scrapebadger --no-save ...` or `SCRAPEBADGER_SAVE=0`.
71
+
72
+ Re-slice a saved result locally, for free:
73
+
74
+ ```bash
75
+ scrapebadger view --summary # newest result: types, list lengths, byte sizes
76
+ scrapebadger view --last google --max-items 5 --fields title,link
77
+ scrapebadger view path/to/result.json --jq '.organic_results[].link' # needs jq on PATH
78
+ ```
79
+
80
+ `--fields` (dotted paths) and `--max-items` apply to the main item list (the
81
+ top-level array, else the largest top-level list); truncation adds
82
+ `"_truncated": {"path", "kept", "total"}`.
83
+
84
+ Re-read a result from the server within ~15 minutes, free, sliced server-side:
85
+
86
+ ```bash
87
+ scrapebadger results get <result_id> --max-items 3 --fields id,text
88
+ ```
89
+
90
+ ## Quotes, bug reports and feedback (all free)
91
+
92
+ ```bash
93
+ scrapebadger quote /v1/google/search -p q="coffee" -p gl=us # cost, validation, latency
94
+ scrapebadger quote /v1/web/scrape --method POST --body '{"url": "https://example.com"}'
95
+ scrapebadger report-bug "Empty results for a public profile" --request-id <X-Request-ID> --endpoint /v1/instagram/users/x
96
+ scrapebadger feedback "Please add Pinterest" --details "..."
97
+ ```
98
+
99
+ Error messages include the `request_id` to quote in a bug report, and the
100
+ upgrade or claim link when you are out of credits.
101
+
102
+ ## Environment
103
+
104
+ | Variable | Default | Purpose |
105
+ | --- | --- | --- |
106
+ | `SCRAPEBADGER_API_KEY` | | API key (wins over `.env` and `~/.config/scrapebadger/config.json`) |
107
+ | `SCRAPEBADGER_API_URL` | `https://scrapebadger.com` | API base URL, e.g. a local stack |
108
+ | `SCRAPEBADGER_SITE_URL` | `https://scrapebadger.com` | Where `init` downloads the skill from |
109
+ | `SCRAPEBADGER_MCP_URL` | `https://mcp.scrapebadger.com/mcp` | MCP server `init` registers |
110
+ | `SCRAPEBADGER_SAVE` | `1` | `0` disables saving results |
111
+
112
+ ## Vinted mobile data (0.14.0+)
113
+
114
+ ```sh
115
+ scrapebadger vinted mobile-operations
116
+ scrapebadger vinted mobile sold-comparables --market fr --params '{"catalog_id":"1242","brand_id":"53","status_id":"2"}'
117
+ scrapebadger vinted search "nike" --sizes 206 --materials 43
118
+ ```
119
+
120
+ The catalog lists 76 public read operations with parameter types and examples.
121
+ Each mobile read costs 1 credit; discovery is free. Sold comparables are a
122
+ Vinted-selected sample, not a complete sales history or guaranteed final sale prices.
123
+
124
+ ### Vinted image search
125
+
126
+ ```bash
127
+ scrapebadger vinted search-by-image --image photo.jpg --market nl --price-to 50
128
+ scrapebadger vinted search-by-image --image-url https://example.com/photo.webp --brand-ids 53
129
+ ```
130
+
131
+ Provide exactly one of `--image`, `--image-url` or `--image-base64`. JPEG, PNG and
132
+ WebP up to 5 MiB are supported. Returns active listings in visual ranking order;
133
+ 10 credits per successful request. Filter with `--catalog-ids`, `--brand-ids`,
134
+ `--status-ids`, `--color-ids`, `--size-ids`, `--material-ids` and price bounds.
135
+ Resend the same image and `--time` from pagination when requesting `--page 2`.
136
+
137
+ ## Dedicated ecommerce APIs
138
+
139
+ ```bash
140
+ scrapebadger etsy search "ceramic mug" --currency USD --country US
141
+ scrapebadger aliexpress search "keyboard"
142
+ scrapebadger target search "laptop" --zip-code 55403 --store-id 1375
143
+ scrapebadger asos search "shoes" --market uk
144
+ scrapebadger bestbuy search "laptop"
145
+ ```
146
+
147
+ Run `scrapebadger <api> --help` to list product, review, store, availability and
148
+ reference commands for that API. Each command supports `--help` for its exact
149
+ parameters. Requests use your API key's existing permissions and credit balance.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "scrapebadger-cli"
3
- version = "0.23.2"
3
+ version = "0.24.0"
4
4
  description = "ScrapeBadger CLI — Twitter, Vinted, Google, and Web Scraping from the command line"
5
5
  requires-python = ">=3.10"
6
6
  license = {text = "MIT"}
@@ -0,0 +1,114 @@
1
+ """HTTP client for ScrapeBadger API."""
2
+
3
+ import os
4
+ from typing import Any
5
+
6
+ import httpx
7
+ import typer
8
+
9
+ from scrapebadger_cli import results
10
+ from scrapebadger_cli.config import get_api_key
11
+
12
+ BASE_URL = os.environ.get("SCRAPEBADGER_API_URL", "https://scrapebadger.com")
13
+ _client: httpx.Client | None = None
14
+
15
+ # Links an error body may carry (402 / zero-credit 429 / trial errors).
16
+ _ERROR_LINKS = ("claim_url", "upgrade_url", "free_credits_url", "pricing_url", "signup_url")
17
+
18
+
19
+ def _new_client(headers: dict[str, str] | None = None) -> httpx.Client:
20
+ return httpx.Client(base_url=BASE_URL, headers=headers, timeout=300.0)
21
+
22
+
23
+ def _get_client() -> httpx.Client:
24
+ global _client
25
+ if _client is None:
26
+ api_key = get_api_key()
27
+ if not api_key:
28
+ typer.echo(
29
+ "No API key found. Set SCRAPEBADGER_API_KEY or run: scrapebadger login"
30
+ " (or scrapebadger trial for a 50-credit key without an account)",
31
+ err=True,
32
+ )
33
+ raise typer.Exit(1)
34
+ _client = _new_client({"X-API-Key": api_key})
35
+ return _client
36
+
37
+
38
+ def api_get(path: str, params: dict[str, Any] | None = None, *, save: bool = True) -> dict | list:
39
+ """Make a GET request to the ScrapeBadger API.
40
+
41
+ ``save=False`` for account/meta calls that are not results worth caching.
42
+ """
43
+ clean = {k: v for k, v in (params or {}).items() if v is not None}
44
+ resp = _get_client().get(path, params=clean or None)
45
+ return _handle_response(resp, path if save else None)
46
+
47
+
48
+ def api_post(
49
+ path: str, json_body: dict[str, Any] | None = None, *, save: bool = True
50
+ ) -> dict | list:
51
+ """Make a POST request to the ScrapeBadger API."""
52
+ resp = _get_client().post(path, json=json_body)
53
+ return _handle_response(resp, path if save else None)
54
+
55
+
56
+ def api_post_raw(path: str, json_body: dict[str, Any] | None = None) -> tuple[bytes, str]:
57
+ """POST and return ``(body_bytes, content_type)`` without JSON-decoding.
58
+
59
+ For `/v1/web/scrape` with `raw_content`, whose response is the scraped body
60
+ itself. Returns bytes, not text: the payload may be an image or a PDF and
61
+ decoding it would corrupt it.
62
+ """
63
+ resp = _get_client().post(path, json=json_body)
64
+ if resp.status_code >= 400:
65
+ _handle_response(resp)
66
+ return resp.content, resp.headers.get("content-type", "")
67
+
68
+
69
+ def public_post(path: str, json_body: dict[str, Any]) -> tuple[int, dict]:
70
+ """POST to an unauthenticated ``/v1/public/*`` endpoint; ``(status, body)``.
71
+
72
+ Errors are returned, not raised: the trial and device flows branch on them.
73
+ """
74
+ with _new_client() as client:
75
+ resp = client.post(path, json=json_body)
76
+ try:
77
+ body = resp.json()
78
+ except ValueError:
79
+ body = {"error": "invalid_response", "detail": resp.text}
80
+ return resp.status_code, body if isinstance(body, dict) else {"data": body}
81
+
82
+
83
+ def error_message(body: Any, fallback: str = "") -> str:
84
+ """One readable message from an error body, plus any links it carries."""
85
+ if not isinstance(body, dict):
86
+ return fallback
87
+ if isinstance(body.get("detail"), dict): # HTTPException(detail={...})
88
+ body = {**body, **body["detail"]}
89
+ message = str(body.get("message") or body.get("detail") or body.get("error") or fallback)
90
+ for key in _ERROR_LINKS:
91
+ url = body.get(key)
92
+ if url and url not in message:
93
+ message += f"\n {key}: {url}"
94
+ return message
95
+
96
+
97
+ def _handle_response(resp: httpx.Response, save_path: str | None = None) -> dict | list:
98
+ """Handle API response, exit on errors; save successful results when asked."""
99
+ if resp.status_code >= 400:
100
+ try:
101
+ body = resp.json()
102
+ except ValueError:
103
+ body = None
104
+ message = error_message(body, resp.text)
105
+ if request_id := resp.headers.get("x-request-id"):
106
+ message += f"\n request_id: {request_id}"
107
+ typer.echo(f"Error ({resp.status_code}): {message}", err=True)
108
+ raise typer.Exit(1)
109
+ if resp.status_code == 204:
110
+ return {"status": "deleted"}
111
+ data = resp.json()
112
+ if save_path:
113
+ results.save(resp, data, save_path)
114
+ return data
@@ -0,0 +1,242 @@
1
+ """Agent helpers: view saved results, re-read results, quote, feedback."""
2
+
3
+ import json
4
+ import shutil
5
+ import subprocess
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ import typer
11
+
12
+ from scrapebadger_cli import results as saved
13
+ from scrapebadger_cli.client import api_get, api_post
14
+ from scrapebadger_cli.output import console, render
15
+ from scrapebadger_cli.views import apply_view, summary
16
+
17
+ results_app = typer.Typer(no_args_is_help=True)
18
+
19
+ FMT = typer.Option("json", "--output", "-o", help="Output format: json, csv, table, markdown")
20
+ JQ = typer.Option(None, "--jq", help="jq expression to run on the result")
21
+ FIELDS = typer.Option(None, "--fields", help="Comma-separated (dotted) fields of each item to keep")
22
+ MAX_ITEMS = typer.Option(None, "--max-items", help="Keep only the first N items of the main list")
23
+ SUMMARY = typer.Option(False, "--summary", help="Structural outline instead of the data")
24
+
25
+
26
+ def _print_json(data: Any) -> None:
27
+ sys.stdout.write(json.dumps(data, indent=2, ensure_ascii=False, default=str) + "\n")
28
+
29
+
30
+ def _run_jq(expr: str, data: Any) -> None:
31
+ if not shutil.which("jq"):
32
+ typer.echo(
33
+ "Error: --jq needs the jq binary on PATH (brew install jq / apt install jq).", err=True
34
+ )
35
+ raise typer.Exit(1)
36
+ proc = subprocess.run(
37
+ ["jq", expr], input=json.dumps(data), capture_output=True, text=True, check=False
38
+ )
39
+ sys.stdout.write(proc.stdout)
40
+ if proc.returncode:
41
+ typer.echo(proc.stderr.rstrip(), err=True)
42
+ raise typer.Exit(proc.returncode)
43
+
44
+
45
+ def _known_apis() -> list[str]:
46
+ from scrapebadger_cli.main import app
47
+
48
+ return [g.name for g in app.registered_groups if g.name]
49
+
50
+
51
+ def _latest(api: str | None) -> Path:
52
+ files = saved.saved_files()
53
+ if api:
54
+ # "google" must not match "google-play-..." files.
55
+ longer = [a for a in _known_apis() if a.startswith(f"{api}-")]
56
+ files = [
57
+ f
58
+ for f in files
59
+ if f.name.startswith(f"{api}-") and not any(f.name.startswith(f"{a}-") for a in longer)
60
+ ]
61
+ if not files:
62
+ where = f" for {api}" if api else ""
63
+ typer.echo(f"Error: no saved results{where} in {saved.results_dir()}", err=True)
64
+ raise typer.Exit(1)
65
+ return files[0]
66
+
67
+
68
+ def view(
69
+ target: str | None = typer.Argument(
70
+ None, help="Saved result file (default: the newest). With --last: an API name filter"
71
+ ),
72
+ last: bool = typer.Option(
73
+ False, "--last", help="Use the newest saved result (of TARGET's API)"
74
+ ),
75
+ jq: str | None = JQ,
76
+ fields: str | None = FIELDS,
77
+ max_items: int | None = MAX_ITEMS,
78
+ show_summary: bool = SUMMARY,
79
+ fmt: str = FMT,
80
+ ) -> None:
81
+ """Re-slice a saved result locally (free, no API call)."""
82
+ if last or target is None:
83
+ path = _latest(target if last else None)
84
+ else:
85
+ path = Path(target).expanduser()
86
+ if not path.exists() and (saved.results_dir() / target).exists():
87
+ path = saved.results_dir() / target
88
+ try:
89
+ data = json.loads(path.read_text())
90
+ except (OSError, ValueError) as exc:
91
+ typer.echo(f"Error: cannot read {path}: {exc}", err=True)
92
+ raise typer.Exit(1) from exc
93
+ typer.echo(f"viewing: {path}", err=True)
94
+
95
+ data = apply_view(data, fields, max_items)
96
+ if show_summary:
97
+ data = summary(data)
98
+ if jq:
99
+ _run_jq(jq, data)
100
+ elif fmt == "json":
101
+ _print_json(data)
102
+ else:
103
+ render(data, fmt)
104
+
105
+
106
+ @results_app.command("get")
107
+ def results_get(
108
+ result_id: str = typer.Argument(..., help="X-Result-ID of an earlier call (kept ~15 min)"),
109
+ jq: str | None = JQ,
110
+ fields: str | None = FIELDS,
111
+ max_items: int | None = MAX_ITEMS,
112
+ show_summary: bool = SUMMARY,
113
+ fmt: str = FMT,
114
+ ) -> None:
115
+ """Re-read an earlier result from the server, free, optionally sliced server-side."""
116
+ data = api_get(
117
+ f"/v1/results/{result_id}",
118
+ {
119
+ "jq": jq,
120
+ "fields": fields,
121
+ "max_items": max_items,
122
+ "summary": "true" if show_summary else None,
123
+ },
124
+ save=False,
125
+ )
126
+ if fmt == "json":
127
+ _print_json(data)
128
+ else:
129
+ render(data, fmt)
130
+
131
+
132
+ def _parse_params(pairs: list[str]) -> dict[str, str]:
133
+ params = {}
134
+ for pair in pairs:
135
+ key, sep, value = pair.partition("=")
136
+ if not sep or not key:
137
+ raise typer.BadParameter(f"expected key=value, got {pair!r}", param_hint="--param")
138
+ params[key] = value
139
+ return params
140
+
141
+
142
+ def quote(
143
+ path: str = typer.Argument(..., help="Endpoint path, e.g. /v1/google/search"),
144
+ param: list[str] = typer.Option([], "--param", "-p", help="Query param key=value (repeatable)"),
145
+ body: str | None = typer.Option(None, "--body", help="JSON request body (POST endpoints)"),
146
+ method: str = typer.Option("GET", "--method", "-X", help="HTTP method of the endpoint"),
147
+ fmt: str = typer.Option("pretty", "--output", "-o", help="pretty or json"),
148
+ ) -> None:
149
+ """Price and validate a call before making it (free, never billed)."""
150
+ try:
151
+ json_body = json.loads(body) if body else None
152
+ except ValueError as exc:
153
+ raise typer.BadParameter(f"not valid JSON: {exc}", param_hint="--body") from exc
154
+ data = api_post(
155
+ "/v1/quote",
156
+ {"method": method.upper(), "path": path, "params": _parse_params(param), "body": json_body},
157
+ save=False,
158
+ )
159
+ if fmt == "json" or not isinstance(data, dict):
160
+ _print_json(data)
161
+ return
162
+
163
+ out = [f"{data.get('method', method)} {data.get('path', path)}"]
164
+ valid = data.get("valid")
165
+ out.append(
166
+ "params: "
167
+ + {True: "valid", False: "INVALID", None: "not validated"}.get(valid, str(valid))
168
+ )
169
+ for err in data.get("errors") or []:
170
+ out.append(f" - {err.get('field')}: {err.get('message')}")
171
+ credits = data.get("credits") or {}
172
+ out.append(f"billing: {data.get('billing')}")
173
+ out.append(
174
+ f"credits: {credits.get('min')}–{credits.get('max') if credits.get('max') is not None else '∞'}"
175
+ f" (base {credits.get('base')}, per item {credits.get('per_item')})"
176
+ )
177
+ if usd := data.get("usd"):
178
+ out.append(
179
+ f"usd: max ${usd.get('max_per_call_list')}/call · "
180
+ f"${usd.get('per_1k_calls_list')}/1k (list) · ${usd.get('per_1k_calls_best')}/1k (best plan)"
181
+ )
182
+ if lat := data.get("latency"):
183
+ out.append(
184
+ f"latency: p50 {lat.get('p50_ms')} ms · p95 {lat.get('p95_ms')} ms · "
185
+ f"p99 {lat.get('p99_ms')} ms ({lat.get('samples')} calls, {lat.get('window_days')} d)"
186
+ )
187
+ else:
188
+ out.append("latency: no samples yet")
189
+ out += [f"note: {n}" for n in data.get("notes") or []]
190
+ typer.echo("\n".join(out))
191
+
192
+
193
+ def _send_feedback(
194
+ kind: str,
195
+ summary_text: str,
196
+ details: str | None,
197
+ request_id: str | None,
198
+ endpoint: str | None,
199
+ contact: str | None,
200
+ ) -> None:
201
+ body = {
202
+ "type": kind,
203
+ "summary": summary_text,
204
+ "details": details,
205
+ "request_id": request_id,
206
+ "endpoint": endpoint,
207
+ "contact": contact,
208
+ }
209
+ data = api_post("/v1/feedback", {k: v for k, v in body.items() if v}, save=False)
210
+ data = data if isinstance(data, dict) else {}
211
+ console.print(f"[green]Received.[/green] [dim]id: {data.get('id', '?')}[/dim]")
212
+ if message := data.get("message"):
213
+ typer.echo(message)
214
+
215
+
216
+ SUMMARY_ARG = typer.Argument(..., help="One-line summary (max 200 chars)")
217
+ DETAILS = typer.Option(None, "--details", "-d", help="Longer description (max 5000 chars)")
218
+ REQUEST_ID = typer.Option(None, "--request-id", help="X-Request-ID of the call concerned")
219
+ ENDPOINT = typer.Option(None, "--endpoint", help="Endpoint concerned, e.g. /v1/google/search")
220
+ CONTACT = typer.Option(None, "--contact", help="How to reach you (optional)")
221
+
222
+
223
+ def report_bug(
224
+ summary_text: str = SUMMARY_ARG,
225
+ details: str | None = DETAILS,
226
+ request_id: str | None = REQUEST_ID,
227
+ endpoint: str | None = ENDPOINT,
228
+ contact: str | None = CONTACT,
229
+ ) -> None:
230
+ """Report a bug (wrong data, an error, a block) to the ScrapeBadger team. Free."""
231
+ _send_feedback("bug", summary_text, details, request_id, endpoint, contact)
232
+
233
+
234
+ def feedback(
235
+ summary_text: str = SUMMARY_ARG,
236
+ details: str | None = DETAILS,
237
+ request_id: str | None = REQUEST_ID,
238
+ endpoint: str | None = ENDPOINT,
239
+ contact: str | None = CONTACT,
240
+ ) -> None:
241
+ """Send feedback or a feature request to the ScrapeBadger team. Free."""
242
+ _send_feedback("feedback", summary_text, details, request_id, endpoint, contact)