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.
- scrapebadger_cli-0.24.0/PKG-INFO +171 -0
- scrapebadger_cli-0.24.0/README.md +149 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/pyproject.toml +1 -1
- scrapebadger_cli-0.24.0/src/scrapebadger_cli/client.py +114 -0
- scrapebadger_cli-0.24.0/src/scrapebadger_cli/commands/agent.py +242 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/auth.py +1 -1
- scrapebadger_cli-0.24.0/src/scrapebadger_cli/commands/init.py +163 -0
- scrapebadger_cli-0.24.0/src/scrapebadger_cli/commands/onboarding.py +182 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/config.py +24 -5
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/main.py +26 -0
- scrapebadger_cli-0.24.0/src/scrapebadger_cli/results.py +76 -0
- scrapebadger_cli-0.24.0/src/scrapebadger_cli/views.py +109 -0
- scrapebadger_cli-0.24.0/tests/test_agent_features.py +350 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/uv.lock +1 -1
- scrapebadger_cli-0.23.2/PKG-INFO +0 -97
- scrapebadger_cli-0.23.2/README.md +0 -75
- scrapebadger_cli-0.23.2/src/scrapebadger_cli/client.py +0 -70
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/.gitignore +0 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/__init__.py +0 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/__init__.py +0 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/airbnb.py +0 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/aliexpress.py +0 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/amazon.py +0 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/apartments.py +0 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/app_store.py +0 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/asos.py +0 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/baidu.py +0 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/bestbuy.py +0 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/bing.py +0 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/booking.py +0 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/chatgpt.py +0 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/depop.py +0 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/duckduckgo.py +0 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/ebay.py +0 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/etsy.py +0 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/facebook.py +0 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/gemini.py +0 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/google.py +0 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/google_play.py +0 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/gumtree.py +0 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/idealista.py +0 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/immobiliare.py +0 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/instagram.py +0 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/leboncoin.py +0 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/linkedin.py +0 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/loopnet.py +0 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/naver.py +0 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/perplexity.py +0 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/realtor.py +0 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/reddit.py +0 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/redfin.py +0 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/stream.py +0 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/target.py +0 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/tiktok.py +0 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/twitter.py +0 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/vinted.py +0 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/wallapop.py +0 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/walmart.py +0 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/web.py +0 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/yahoo.py +0 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/yandex.py +0 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/youtube.py +0 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/commands/zillow.py +0 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/src/scrapebadger_cli/output.py +0 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/tests/test_google_light_search.py +0 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/tests/test_naver.py +0 -0
- {scrapebadger_cli-0.23.2 → scrapebadger_cli-0.24.0}/tests/test_tiktok_pagination.py +0 -0
- {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.
|
|
@@ -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)
|