scrapebadger-cli 0.26.0__tar.gz → 0.27.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.26.0 → scrapebadger_cli-0.27.0}/PKG-INFO +46 -1
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/README.md +45 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/pyproject.toml +1 -1
- scrapebadger_cli-0.27.0/src/scrapebadger_cli/commands/autoscout24.py +88 -0
- scrapebadger_cli-0.27.0/src/scrapebadger_cli/commands/brave.py +163 -0
- scrapebadger_cli-0.27.0/src/scrapebadger_cli/commands/github.py +40 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/main.py +6 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/output.py +12 -1
- scrapebadger_cli-0.27.0/tests/conftest.py +6 -0
- scrapebadger_cli-0.27.0/tests/test_autoscout24_commands.py +76 -0
- scrapebadger_cli-0.27.0/tests/test_brave.py +117 -0
- scrapebadger_cli-0.27.0/tests/test_github.py +69 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/.gitignore +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/__init__.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/client.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/__init__.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/agent.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/airbnb.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/aliexpress.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/amazon.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/apartments.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/app_store.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/asos.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/auth.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/baidu.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/bestbuy.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/bing.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/booking.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/chatgpt.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/copilot.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/depop.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/duckduckgo.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/ebay.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/etsy.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/facebook.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/gemini.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/google.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/google_play.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/gumtree.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/idealista.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/immobiliare.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/init.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/instagram.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/leboncoin.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/linkedin.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/loopnet.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/naver.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/onboarding.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/perplexity.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/realtor.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/reddit.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/redfin.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/stream.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/target.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/tiktok.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/twitter.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/vinted.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/wallapop.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/walmart.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/web.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/yahoo.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/yandex.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/youtube.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/zillow.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/config.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/results.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/views.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/tests/test_agent_features.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/tests/test_copilot_commands.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/tests/test_google_light_search.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/tests/test_naver.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/tests/test_perplexity_strict_country.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/tests/test_tiktok_pagination.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/tests/test_vinted_image_search.py +0 -0
- {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: scrapebadger-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.27.0
|
|
4
4
|
Summary: ScrapeBadger CLI — Twitter, Vinted, Google, and Web Scraping from the command line
|
|
5
5
|
Author: ScrapeBadger Team
|
|
6
6
|
License: MIT
|
|
@@ -169,3 +169,48 @@ scrapebadger bestbuy search "laptop"
|
|
|
169
169
|
Run `scrapebadger <api> --help` to list product, review, store, availability and
|
|
170
170
|
reference commands for that API. Each command supports `--help` for its exact
|
|
171
171
|
parameters. Requests use your API key's existing permissions and credit balance.
|
|
172
|
+
|
|
173
|
+
### GitHub public profiles
|
|
174
|
+
|
|
175
|
+
Prepared for the GitHub API rollout. Use a login or @handle, not a URL.
|
|
176
|
+
All modes cost 10 credits per successful request. `--year` is overview-only,
|
|
177
|
+
`--page` repositories-only, and `--next-page-token` stars-only (users only).
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
scrapebadger github profile sindresorhus --year 2024
|
|
181
|
+
scrapebadger github profile rails --tab repositories --page 2
|
|
182
|
+
scrapebadger github profile sindresorhus --tab stars --next-page-token OPAQUE_TOKEN
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
These examples are representative; see https://docs.scrapebadger.com/github/profiles.
|
|
186
|
+
|
|
187
|
+
### AutoScout24 vehicle search
|
|
188
|
+
|
|
189
|
+
Search vehicle cards across eight European marketplaces. Listing detail is not included.
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
scrapebadger autoscout24 search --market de --make volkswagen --model golf --price-max 20000
|
|
193
|
+
scrapebadger autoscout24 search --market it --make volkswagen --model golf --sort-by price_asc --page 2
|
|
194
|
+
scrapebadger autoscout24 filters --market de --make volkswagen
|
|
195
|
+
scrapebadger autoscout24 markets
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Ranges use `--price-min/--price-max` (EUR), `--mileage-min/--mileage-max` (km),
|
|
199
|
+
`--year-min/--year-max` (first registration) and `--power-min/--power-max` (kW).
|
|
200
|
+
Use `--help` for fuel, transmission, body, seller, condition and postcode/radius filters.
|
|
201
|
+
Follow the response's `next_page`; the source caps browsing at 200 pages per query.
|
|
202
|
+
## Brave Search
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
scrapebadger brave search "coffee machine" --country de --ui-language de-de --freshness week
|
|
206
|
+
scrapebadger brave images "golden retriever" --count 10
|
|
207
|
+
scrapebadger brave videos "espresso tutorial" --page 2
|
|
208
|
+
scrapebadger brave news "technology" --count 10
|
|
209
|
+
scrapebadger brave autocomplete "coff"
|
|
210
|
+
scrapebadger brave locales
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Web/video `--page` is a 1-based native page ordinal; `--count` trims one source
|
|
214
|
+
page without changing its boundaries. Images/news return an initial batch.
|
|
215
|
+
JSON output preserves the complete response, including rich blocks and native
|
|
216
|
+
query alteration metadata. Use `brave locales` for country/UI-language codes.
|
|
@@ -147,3 +147,48 @@ scrapebadger bestbuy search "laptop"
|
|
|
147
147
|
Run `scrapebadger <api> --help` to list product, review, store, availability and
|
|
148
148
|
reference commands for that API. Each command supports `--help` for its exact
|
|
149
149
|
parameters. Requests use your API key's existing permissions and credit balance.
|
|
150
|
+
|
|
151
|
+
### GitHub public profiles
|
|
152
|
+
|
|
153
|
+
Prepared for the GitHub API rollout. Use a login or @handle, not a URL.
|
|
154
|
+
All modes cost 10 credits per successful request. `--year` is overview-only,
|
|
155
|
+
`--page` repositories-only, and `--next-page-token` stars-only (users only).
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
scrapebadger github profile sindresorhus --year 2024
|
|
159
|
+
scrapebadger github profile rails --tab repositories --page 2
|
|
160
|
+
scrapebadger github profile sindresorhus --tab stars --next-page-token OPAQUE_TOKEN
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
These examples are representative; see https://docs.scrapebadger.com/github/profiles.
|
|
164
|
+
|
|
165
|
+
### AutoScout24 vehicle search
|
|
166
|
+
|
|
167
|
+
Search vehicle cards across eight European marketplaces. Listing detail is not included.
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
scrapebadger autoscout24 search --market de --make volkswagen --model golf --price-max 20000
|
|
171
|
+
scrapebadger autoscout24 search --market it --make volkswagen --model golf --sort-by price_asc --page 2
|
|
172
|
+
scrapebadger autoscout24 filters --market de --make volkswagen
|
|
173
|
+
scrapebadger autoscout24 markets
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Ranges use `--price-min/--price-max` (EUR), `--mileage-min/--mileage-max` (km),
|
|
177
|
+
`--year-min/--year-max` (first registration) and `--power-min/--power-max` (kW).
|
|
178
|
+
Use `--help` for fuel, transmission, body, seller, condition and postcode/radius filters.
|
|
179
|
+
Follow the response's `next_page`; the source caps browsing at 200 pages per query.
|
|
180
|
+
## Brave Search
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
scrapebadger brave search "coffee machine" --country de --ui-language de-de --freshness week
|
|
184
|
+
scrapebadger brave images "golden retriever" --count 10
|
|
185
|
+
scrapebadger brave videos "espresso tutorial" --page 2
|
|
186
|
+
scrapebadger brave news "technology" --count 10
|
|
187
|
+
scrapebadger brave autocomplete "coff"
|
|
188
|
+
scrapebadger brave locales
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Web/video `--page` is a 1-based native page ordinal; `--count` trims one source
|
|
192
|
+
page without changing its boundaries. Images/news return an initial batch.
|
|
193
|
+
JSON output preserves the complete response, including rich blocks and native
|
|
194
|
+
query alteration metadata. Use `brave locales` for country/UI-language codes.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""AutoScout24 vehicle listing search commands."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from scrapebadger_cli.client import api_get
|
|
6
|
+
from scrapebadger_cli.output import render
|
|
7
|
+
|
|
8
|
+
app = typer.Typer(no_args_is_help=True)
|
|
9
|
+
FMT = typer.Option("json", "--output", "-o", help="Output format: json, csv, table, markdown")
|
|
10
|
+
FIELDS = typer.Option(None, "--fields", "-f", help="Comma-separated response fields")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@app.command()
|
|
14
|
+
def search(
|
|
15
|
+
market: str = typer.Option("de", "--market", help="de, at, be, es, fr, it, lu or nl"),
|
|
16
|
+
make: str | None = typer.Option(None, "--make", help="Native make slug, e.g. volkswagen"),
|
|
17
|
+
model: str | None = typer.Option(
|
|
18
|
+
None, "--model", help="Native model slug, e.g. golf; requires make"
|
|
19
|
+
),
|
|
20
|
+
price_min: int | None = typer.Option(None, "--price-min", help="price min"),
|
|
21
|
+
price_max: int | None = typer.Option(None, "--price-max", help="price max"),
|
|
22
|
+
mileage_min: int | None = typer.Option(None, "--mileage-min", help="mileage min"),
|
|
23
|
+
mileage_max: int | None = typer.Option(None, "--mileage-max", help="mileage max"),
|
|
24
|
+
year_min: int | None = typer.Option(None, "--year-min", help="year min"),
|
|
25
|
+
year_max: int | None = typer.Option(None, "--year-max", help="year max"),
|
|
26
|
+
power_min: int | None = typer.Option(None, "--power-min", help="Minimum power in kW"),
|
|
27
|
+
power_max: int | None = typer.Option(None, "--power-max", help="Maximum power in kW"),
|
|
28
|
+
fuel_type: str | None = typer.Option(None, "--fuel-type", help="fuel type"),
|
|
29
|
+
transmission: str | None = typer.Option(None, "--transmission", help="transmission"),
|
|
30
|
+
body_type: str | None = typer.Option(None, "--body-type", help="body type"),
|
|
31
|
+
seller_type: str | None = typer.Option(None, "--seller-type", help="seller type"),
|
|
32
|
+
condition: str | None = typer.Option(None, "--condition", help="condition"),
|
|
33
|
+
zip: str | None = typer.Option(None, "--zip", help="zip"),
|
|
34
|
+
radius: int | None = typer.Option(None, "--radius", help="radius"),
|
|
35
|
+
sort_by: str = typer.Option("best_match", "--sort-by", help="sort by"),
|
|
36
|
+
page: int = typer.Option(1, "--page", help="Page 1–200; follow next_page"),
|
|
37
|
+
fmt: str = FMT,
|
|
38
|
+
fields: str | None = FIELDS,
|
|
39
|
+
) -> None:
|
|
40
|
+
"""Search live vehicle listings. 5 credits; genuine empty results free."""
|
|
41
|
+
data = api_get(
|
|
42
|
+
"/v1/autoscout24/search",
|
|
43
|
+
{
|
|
44
|
+
"market": market,
|
|
45
|
+
"make": make,
|
|
46
|
+
"model": model,
|
|
47
|
+
"price_min": price_min,
|
|
48
|
+
"price_max": price_max,
|
|
49
|
+
"mileage_min": mileage_min,
|
|
50
|
+
"mileage_max": mileage_max,
|
|
51
|
+
"year_min": year_min,
|
|
52
|
+
"year_max": year_max,
|
|
53
|
+
"power_min": power_min,
|
|
54
|
+
"power_max": power_max,
|
|
55
|
+
"fuel_type": fuel_type,
|
|
56
|
+
"transmission": transmission,
|
|
57
|
+
"body_type": body_type,
|
|
58
|
+
"seller_type": seller_type,
|
|
59
|
+
"condition": condition,
|
|
60
|
+
"zip": zip,
|
|
61
|
+
"radius": radius,
|
|
62
|
+
"sort_by": sort_by,
|
|
63
|
+
"page": page,
|
|
64
|
+
},
|
|
65
|
+
)
|
|
66
|
+
render(data, fmt, fields, preserve_envelope=True)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@app.command()
|
|
70
|
+
def filters(
|
|
71
|
+
market: str = typer.Option("de", "--market"),
|
|
72
|
+
make: str | None = typer.Option(None, "--make"),
|
|
73
|
+
fmt: str = FMT,
|
|
74
|
+
fields: str | None = FIELDS,
|
|
75
|
+
) -> None:
|
|
76
|
+
"""Current native vehicle filters. 2 credits."""
|
|
77
|
+
render(
|
|
78
|
+
api_get("/v1/autoscout24/filters", {"market": market, "make": make}),
|
|
79
|
+
fmt,
|
|
80
|
+
fields,
|
|
81
|
+
preserve_envelope=True,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@app.command()
|
|
86
|
+
def markets(fmt: str = FMT, fields: str | None = FIELDS) -> None:
|
|
87
|
+
"""List eight supported European marketplaces. Free."""
|
|
88
|
+
render(api_get("/v1/autoscout24/markets"), fmt, fields, preserve_envelope=True)
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Native Brave Search commands; page ordinals and locale controls match the API."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from scrapebadger_cli.client import api_get
|
|
6
|
+
from scrapebadger_cli.output import render
|
|
7
|
+
|
|
8
|
+
app = typer.Typer(no_args_is_help=True)
|
|
9
|
+
|
|
10
|
+
FMT = typer.Option("json", "--output", "-o", help="Output: json, csv, table, markdown")
|
|
11
|
+
FIELDS = typer.Option(None, "--fields", "-f", help="Comma-separated response fields")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@app.command()
|
|
15
|
+
def search(
|
|
16
|
+
query: str = typer.Argument(..., help="Search keywords"),
|
|
17
|
+
country: str = typer.Option(
|
|
18
|
+
"us", "--country", help="Result region; use locales for valid codes"
|
|
19
|
+
),
|
|
20
|
+
ui_language: str = typer.Option(
|
|
21
|
+
"en-us", "--ui-language", help="UI-label language; does not restrict result language"
|
|
22
|
+
),
|
|
23
|
+
safe_search: str = typer.Option("moderate", "--safe-search", help="off, moderate, strict"),
|
|
24
|
+
page: int = typer.Option(1, "--page", help="1-based native page ordinal (1–10)"),
|
|
25
|
+
count: int = typer.Option(
|
|
26
|
+
20, "--count", help="Trim one source page/batch; does not change page boundaries"
|
|
27
|
+
),
|
|
28
|
+
freshness: str = typer.Option("any", "--freshness", help="any, day, week, month, year"),
|
|
29
|
+
spellcheck: bool = typer.Option(
|
|
30
|
+
False, "--spellcheck/--no-spellcheck", help="Allow native spelling correction"
|
|
31
|
+
),
|
|
32
|
+
fmt: str = FMT,
|
|
33
|
+
fields: str | None = FIELDS,
|
|
34
|
+
) -> None:
|
|
35
|
+
"""Search native Brave web results and rich blocks. Page is a 1-based native page; count trims one page. Seven credits."""
|
|
36
|
+
data = api_get(
|
|
37
|
+
"/v1/brave/search",
|
|
38
|
+
{
|
|
39
|
+
"query": query,
|
|
40
|
+
"country": country,
|
|
41
|
+
"ui_language": ui_language,
|
|
42
|
+
"safe_search": safe_search,
|
|
43
|
+
"page": page,
|
|
44
|
+
"count": count,
|
|
45
|
+
"freshness": freshness,
|
|
46
|
+
"spellcheck": spellcheck,
|
|
47
|
+
},
|
|
48
|
+
)
|
|
49
|
+
render(data, fmt, fields, preserve_envelope=True)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@app.command()
|
|
53
|
+
def images(
|
|
54
|
+
query: str = typer.Argument(..., help="Search keywords"),
|
|
55
|
+
country: str = typer.Option(
|
|
56
|
+
"us", "--country", help="Result region; use locales for valid codes"
|
|
57
|
+
),
|
|
58
|
+
ui_language: str = typer.Option(
|
|
59
|
+
"en-us", "--ui-language", help="UI-label language; does not restrict result language"
|
|
60
|
+
),
|
|
61
|
+
safe_search: str = typer.Option("moderate", "--safe-search", help="off, moderate, strict"),
|
|
62
|
+
count: int = typer.Option(
|
|
63
|
+
50, "--count", help="Trim one source page/batch; does not change page boundaries"
|
|
64
|
+
),
|
|
65
|
+
fmt: str = FMT,
|
|
66
|
+
fields: str | None = FIELDS,
|
|
67
|
+
) -> None:
|
|
68
|
+
"""Search the initial native Brave image batch. Count trims the batch; no pagination. Seven credits."""
|
|
69
|
+
data = api_get(
|
|
70
|
+
"/v1/brave/images",
|
|
71
|
+
{
|
|
72
|
+
"query": query,
|
|
73
|
+
"country": country,
|
|
74
|
+
"ui_language": ui_language,
|
|
75
|
+
"safe_search": safe_search,
|
|
76
|
+
"count": count,
|
|
77
|
+
},
|
|
78
|
+
)
|
|
79
|
+
render(data, fmt, fields, preserve_envelope=True)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@app.command()
|
|
83
|
+
def videos(
|
|
84
|
+
query: str = typer.Argument(..., help="Search keywords"),
|
|
85
|
+
country: str = typer.Option(
|
|
86
|
+
"us", "--country", help="Result region; use locales for valid codes"
|
|
87
|
+
),
|
|
88
|
+
ui_language: str = typer.Option(
|
|
89
|
+
"en-us", "--ui-language", help="UI-label language; does not restrict result language"
|
|
90
|
+
),
|
|
91
|
+
safe_search: str = typer.Option("moderate", "--safe-search", help="off, moderate, strict"),
|
|
92
|
+
page: int = typer.Option(1, "--page", help="1-based native page ordinal (1–10)"),
|
|
93
|
+
count: int = typer.Option(
|
|
94
|
+
50, "--count", help="Trim one source page/batch; does not change page boundaries"
|
|
95
|
+
),
|
|
96
|
+
fmt: str = FMT,
|
|
97
|
+
fields: str | None = FIELDS,
|
|
98
|
+
) -> None:
|
|
99
|
+
"""Search native Brave videos using 1-based page ordinals. Count trims one page. Seven credits."""
|
|
100
|
+
data = api_get(
|
|
101
|
+
"/v1/brave/videos",
|
|
102
|
+
{
|
|
103
|
+
"query": query,
|
|
104
|
+
"country": country,
|
|
105
|
+
"ui_language": ui_language,
|
|
106
|
+
"safe_search": safe_search,
|
|
107
|
+
"page": page,
|
|
108
|
+
"count": count,
|
|
109
|
+
},
|
|
110
|
+
)
|
|
111
|
+
render(data, fmt, fields, preserve_envelope=True)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@app.command()
|
|
115
|
+
def news(
|
|
116
|
+
query: str = typer.Argument(..., help="Search keywords"),
|
|
117
|
+
country: str = typer.Option(
|
|
118
|
+
"us", "--country", help="Result region; use locales for valid codes"
|
|
119
|
+
),
|
|
120
|
+
ui_language: str = typer.Option(
|
|
121
|
+
"en-us", "--ui-language", help="UI-label language; does not restrict result language"
|
|
122
|
+
),
|
|
123
|
+
safe_search: str = typer.Option("moderate", "--safe-search", help="off, moderate, strict"),
|
|
124
|
+
count: int = typer.Option(
|
|
125
|
+
35, "--count", help="Trim one source page/batch; does not change page boundaries"
|
|
126
|
+
),
|
|
127
|
+
fmt: str = FMT,
|
|
128
|
+
fields: str | None = FIELDS,
|
|
129
|
+
) -> None:
|
|
130
|
+
"""Search the initial native Brave news batch. Relative source ages stay display strings. Seven credits."""
|
|
131
|
+
data = api_get(
|
|
132
|
+
"/v1/brave/news",
|
|
133
|
+
{
|
|
134
|
+
"query": query,
|
|
135
|
+
"country": country,
|
|
136
|
+
"ui_language": ui_language,
|
|
137
|
+
"safe_search": safe_search,
|
|
138
|
+
"count": count,
|
|
139
|
+
},
|
|
140
|
+
)
|
|
141
|
+
render(data, fmt, fields, preserve_envelope=True)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@app.command()
|
|
145
|
+
def locales(
|
|
146
|
+
fmt: str = FMT,
|
|
147
|
+
fields: str | None = FIELDS,
|
|
148
|
+
) -> None:
|
|
149
|
+
"""List native Brave country and UI-language choices. Free."""
|
|
150
|
+
data = api_get("/v1/brave/locales")
|
|
151
|
+
render(data, fmt, fields, preserve_envelope=True)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@app.command()
|
|
155
|
+
def autocomplete(
|
|
156
|
+
query: str = typer.Argument(..., help="Partial search query"),
|
|
157
|
+
country: str = typer.Option("us", "--country", help="Native region code; see locales"),
|
|
158
|
+
fmt: str = FMT,
|
|
159
|
+
fields: str | None = FIELDS,
|
|
160
|
+
) -> None:
|
|
161
|
+
"""Get native Brave search-box suggestions and entity metadata. Two credits."""
|
|
162
|
+
data = api_get("/v1/brave/autocomplete", {"query": query, "country": country})
|
|
163
|
+
render(data, fmt, fields, preserve_envelope=True)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""GitHub public profile commands."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from urllib.parse import quote
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
from scrapebadger_cli.client import api_get
|
|
9
|
+
from scrapebadger_cli.output import render
|
|
10
|
+
|
|
11
|
+
app = typer.Typer(no_args_is_help=True)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ProfileTab(str, Enum):
|
|
15
|
+
overview = "overview"
|
|
16
|
+
repositories = "repositories"
|
|
17
|
+
stars = "stars"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@app.command()
|
|
21
|
+
def profile(
|
|
22
|
+
username: str = typer.Argument(..., help="GitHub login or @handle, not a profile URL"),
|
|
23
|
+
tab: ProfileTab = typer.Option(
|
|
24
|
+
ProfileTab.overview, "--tab", help="Profile tab; stars is user-only"
|
|
25
|
+
),
|
|
26
|
+
year: int | None = typer.Option(None, "--year", help="Overview only: whole calendar year"),
|
|
27
|
+
page: int | None = typer.Option(None, "--page", min=1, help="Repositories only: page number"),
|
|
28
|
+
next_page_token: str | None = typer.Option(
|
|
29
|
+
None, "--next-page-token", help="Stars only: opaque next-page token"
|
|
30
|
+
),
|
|
31
|
+
fmt: str = typer.Option("json", "--output", "-o", help="json, csv, table, markdown"),
|
|
32
|
+
fields: str | None = typer.Option(None, "--fields", "-f", help="Comma-separated output fields"),
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Get a public user or organization profile. All tabs cost 10 credits."""
|
|
35
|
+
params = {"tab": tab.value, "year": year, "page": page, "next_page_token": next_page_token}
|
|
36
|
+
data = api_get(
|
|
37
|
+
f"/v1/github/profiles/{quote(username, safe='')}",
|
|
38
|
+
{key: value for key, value in params.items() if value is not None},
|
|
39
|
+
)
|
|
40
|
+
render(data, fmt, fields, preserve_envelope=True)
|
|
@@ -12,10 +12,12 @@ from scrapebadger_cli.commands import (
|
|
|
12
12
|
app_store,
|
|
13
13
|
asos,
|
|
14
14
|
auth,
|
|
15
|
+
autoscout24,
|
|
15
16
|
baidu,
|
|
16
17
|
bestbuy,
|
|
17
18
|
bing,
|
|
18
19
|
booking,
|
|
20
|
+
brave,
|
|
19
21
|
chatgpt,
|
|
20
22
|
copilot,
|
|
21
23
|
depop,
|
|
@@ -24,6 +26,7 @@ from scrapebadger_cli.commands import (
|
|
|
24
26
|
etsy,
|
|
25
27
|
facebook,
|
|
26
28
|
gemini,
|
|
29
|
+
github,
|
|
27
30
|
google,
|
|
28
31
|
google_play,
|
|
29
32
|
gumtree,
|
|
@@ -75,6 +78,7 @@ def _main(
|
|
|
75
78
|
|
|
76
79
|
app.add_typer(twitter.app, name="twitter", help="Twitter/X data commands")
|
|
77
80
|
app.add_typer(reddit.app, name="reddit", help="Reddit data commands")
|
|
81
|
+
app.add_typer(github.app, name="github", help="GitHub public profile commands")
|
|
78
82
|
app.add_typer(instagram.app, name="instagram", help="Instagram data commands")
|
|
79
83
|
app.add_typer(stream.app, name="stream", help="Real-time streaming commands")
|
|
80
84
|
app.add_typer(vinted.app, name="vinted", help="Vinted marketplace commands")
|
|
@@ -97,6 +101,7 @@ app.add_typer(youtube.app, name="youtube", help="YouTube commands")
|
|
|
97
101
|
app.add_typer(zillow.app, name="zillow", help="Zillow real-estate commands")
|
|
98
102
|
app.add_typer(web.app, name="web", help="Web scraping commands")
|
|
99
103
|
app.add_typer(google.app, name="google", help="Google scraper commands")
|
|
104
|
+
app.add_typer(brave.app, name="brave", help="Brave native web, image, video and news search")
|
|
100
105
|
app.add_typer(bing.app, name="bing", help="Bing search engine commands")
|
|
101
106
|
app.add_typer(yahoo.app, name="yahoo", help="Yahoo search engine commands")
|
|
102
107
|
app.add_typer(baidu.app, name="baidu", help="Baidu search engine commands")
|
|
@@ -116,6 +121,7 @@ app.add_typer(apartments.app, name="apartments", help="Apartments.com rental com
|
|
|
116
121
|
app.add_typer(linkedin.app, name="linkedin", help="LinkedIn public data commands")
|
|
117
122
|
app.add_typer(chatgpt.app, name="chatgpt", help="ChatGPT LLM scraping commands")
|
|
118
123
|
app.add_typer(perplexity.app, name="perplexity", help="Perplexity AI answer-engine commands")
|
|
124
|
+
app.add_typer(autoscout24.app, name="autoscout24", help="AutoScout24 vehicle search commands")
|
|
119
125
|
app.add_typer(copilot.app, name="copilot", help="Microsoft Copilot answer-engine commands")
|
|
120
126
|
app.add_typer(gemini.app, name="gemini", help="Google Gemini LLM commands")
|
|
121
127
|
app.add_typer(
|
|
@@ -13,14 +13,25 @@ console = Console()
|
|
|
13
13
|
OutputFormat = str # "json" | "csv" | "table" | "markdown"
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
def render(
|
|
16
|
+
def render(
|
|
17
|
+
data: Any,
|
|
18
|
+
fmt: str = "json",
|
|
19
|
+
fields: str | None = None,
|
|
20
|
+
*,
|
|
21
|
+
preserve_envelope: bool = False,
|
|
22
|
+
) -> None:
|
|
17
23
|
"""Render API response in the chosen format.
|
|
18
24
|
|
|
19
25
|
Args:
|
|
20
26
|
data: The API response (dict or list).
|
|
21
27
|
fmt: Output format — json, csv, table, markdown.
|
|
22
28
|
fields: Comma-separated field names to include (optional).
|
|
29
|
+
preserve_envelope: Keep JSON metadata for products with paginated/rich responses.
|
|
23
30
|
"""
|
|
31
|
+
if fmt == "json" and preserve_envelope and fields is None:
|
|
32
|
+
_render_json(data, data)
|
|
33
|
+
return
|
|
34
|
+
|
|
24
35
|
rows = _normalize_rows(data)
|
|
25
36
|
selected_fields = fields.split(",") if fields else None
|
|
26
37
|
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from unittest.mock import Mock
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
from scrapebadger_cli.commands import autoscout24
|
|
6
|
+
from typer.testing import CliRunner
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_vehicle_ranges_and_pagination_reach_gateway(monkeypatch):
|
|
10
|
+
call = Mock(return_value={"results": []})
|
|
11
|
+
monkeypatch.setattr(autoscout24, "api_get", call)
|
|
12
|
+
monkeypatch.setattr(autoscout24, "render", Mock())
|
|
13
|
+
result = CliRunner().invoke(
|
|
14
|
+
autoscout24.app,
|
|
15
|
+
[
|
|
16
|
+
"search",
|
|
17
|
+
"--market",
|
|
18
|
+
"it",
|
|
19
|
+
"--make",
|
|
20
|
+
"volkswagen",
|
|
21
|
+
"--model",
|
|
22
|
+
"golf",
|
|
23
|
+
"--price-min",
|
|
24
|
+
"0",
|
|
25
|
+
"--price-max",
|
|
26
|
+
"20000",
|
|
27
|
+
"--mileage-max",
|
|
28
|
+
"80000",
|
|
29
|
+
"--year-min",
|
|
30
|
+
"2018",
|
|
31
|
+
"--power-min",
|
|
32
|
+
"75",
|
|
33
|
+
"--fuel-type",
|
|
34
|
+
"diesel",
|
|
35
|
+
"--seller-type",
|
|
36
|
+
"private",
|
|
37
|
+
"--sort-by",
|
|
38
|
+
"price_asc",
|
|
39
|
+
"--page",
|
|
40
|
+
"2",
|
|
41
|
+
],
|
|
42
|
+
)
|
|
43
|
+
assert result.exit_code == 0, result.output
|
|
44
|
+
path, params = call.call_args.args
|
|
45
|
+
assert path == "/v1/autoscout24/search"
|
|
46
|
+
assert params["price_min"] == 0
|
|
47
|
+
assert params["market"] == "it" and params["page"] == 2
|
|
48
|
+
assert params["model"] == "golf" and params["power_min"] == 75
|
|
49
|
+
assert params["sort_by"] == "price_asc" and params["seller_type"] == "private"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_markets_remains_a_static_reference_call(monkeypatch):
|
|
53
|
+
call = Mock(return_value={"markets": []})
|
|
54
|
+
monkeypatch.setattr(autoscout24, "api_get", call)
|
|
55
|
+
monkeypatch.setattr(autoscout24, "render", Mock())
|
|
56
|
+
result = CliRunner().invoke(autoscout24.app, ["markets"])
|
|
57
|
+
assert result.exit_code == 0, result.output
|
|
58
|
+
call.assert_called_once_with("/v1/autoscout24/markets")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@pytest.mark.parametrize("count", [0, 1, 2])
|
|
62
|
+
def test_json_preserves_pagination_for_every_row_count(monkeypatch, count):
|
|
63
|
+
data = {
|
|
64
|
+
"results": [{"id": str(index), "price": 10000} for index in range(count)],
|
|
65
|
+
"market": "de",
|
|
66
|
+
"total_results": 43,
|
|
67
|
+
"total_pages": 3,
|
|
68
|
+
"page": 2 if count else 5,
|
|
69
|
+
"has_next_page": bool(count),
|
|
70
|
+
"next_page": 3 if count else None,
|
|
71
|
+
"page_size": 20,
|
|
72
|
+
}
|
|
73
|
+
monkeypatch.setattr(autoscout24, "api_get", Mock(return_value=data))
|
|
74
|
+
result = CliRunner().invoke(autoscout24.app, ["search"])
|
|
75
|
+
assert result.exit_code == 0, result.output
|
|
76
|
+
assert json.loads(result.stdout) == data
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""CLI adapters must preserve native controls, false values, and full response JSON."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from unittest.mock import Mock
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
from scrapebadger_cli.commands import brave
|
|
8
|
+
from typer.testing import CliRunner
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.mark.parametrize(
|
|
12
|
+
("args", "endpoint", "expected"),
|
|
13
|
+
[
|
|
14
|
+
(
|
|
15
|
+
[
|
|
16
|
+
"search",
|
|
17
|
+
"café & tea",
|
|
18
|
+
"--country",
|
|
19
|
+
"de",
|
|
20
|
+
"--ui-language",
|
|
21
|
+
"de-de",
|
|
22
|
+
"--safe-search",
|
|
23
|
+
"strict",
|
|
24
|
+
"--page",
|
|
25
|
+
"2",
|
|
26
|
+
"--count",
|
|
27
|
+
"5",
|
|
28
|
+
"--freshness",
|
|
29
|
+
"week",
|
|
30
|
+
"--no-spellcheck",
|
|
31
|
+
],
|
|
32
|
+
"search",
|
|
33
|
+
{
|
|
34
|
+
"query": "café & tea",
|
|
35
|
+
"country": "de",
|
|
36
|
+
"ui_language": "de-de",
|
|
37
|
+
"safe_search": "strict",
|
|
38
|
+
"page": 2,
|
|
39
|
+
"count": 5,
|
|
40
|
+
"freshness": "week",
|
|
41
|
+
"spellcheck": False,
|
|
42
|
+
},
|
|
43
|
+
),
|
|
44
|
+
(
|
|
45
|
+
["images", "cats", "--count", "7"],
|
|
46
|
+
"images",
|
|
47
|
+
{
|
|
48
|
+
"query": "cats",
|
|
49
|
+
"country": "us",
|
|
50
|
+
"ui_language": "en-us",
|
|
51
|
+
"safe_search": "moderate",
|
|
52
|
+
"count": 7,
|
|
53
|
+
},
|
|
54
|
+
),
|
|
55
|
+
(
|
|
56
|
+
["videos", "coffee", "--page", "2"],
|
|
57
|
+
"videos",
|
|
58
|
+
{
|
|
59
|
+
"query": "coffee",
|
|
60
|
+
"country": "us",
|
|
61
|
+
"ui_language": "en-us",
|
|
62
|
+
"safe_search": "moderate",
|
|
63
|
+
"page": 2,
|
|
64
|
+
"count": 50,
|
|
65
|
+
},
|
|
66
|
+
),
|
|
67
|
+
(
|
|
68
|
+
["news", "tech"],
|
|
69
|
+
"news",
|
|
70
|
+
{
|
|
71
|
+
"query": "tech",
|
|
72
|
+
"country": "us",
|
|
73
|
+
"ui_language": "en-us",
|
|
74
|
+
"safe_search": "moderate",
|
|
75
|
+
"count": 35,
|
|
76
|
+
},
|
|
77
|
+
),
|
|
78
|
+
(
|
|
79
|
+
["autocomplete", "coff", "--country", "de"],
|
|
80
|
+
"autocomplete",
|
|
81
|
+
{"query": "coff", "country": "de"},
|
|
82
|
+
),
|
|
83
|
+
(["locales"], "locales", None),
|
|
84
|
+
],
|
|
85
|
+
)
|
|
86
|
+
def test_brave_commands(monkeypatch, args, endpoint, expected):
|
|
87
|
+
payload = {
|
|
88
|
+
"results": [{"title": "Café", "additive": {"value": 0}}],
|
|
89
|
+
"query_info": {"altered": False},
|
|
90
|
+
}
|
|
91
|
+
get = Mock(return_value=payload)
|
|
92
|
+
output = Mock()
|
|
93
|
+
monkeypatch.setattr(brave, "api_get", get)
|
|
94
|
+
monkeypatch.setattr(brave, "render", output)
|
|
95
|
+
result = CliRunner().invoke(brave.app, args)
|
|
96
|
+
assert result.exit_code == 0, result.output
|
|
97
|
+
if expected is None:
|
|
98
|
+
get.assert_called_once_with(f"/v1/brave/{endpoint}")
|
|
99
|
+
else:
|
|
100
|
+
get.assert_called_once_with(f"/v1/brave/{endpoint}", expected)
|
|
101
|
+
output.assert_called_once_with(payload, "json", None, preserve_envelope=True)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@pytest.mark.parametrize("count", [0, 1, 2])
|
|
105
|
+
def test_json_retains_envelope_for_every_result_count(monkeypatch, count):
|
|
106
|
+
payload = {
|
|
107
|
+
"results": [{"title": "Café", "url": f"https://example.org/{i}"} for i in range(count)],
|
|
108
|
+
"result_count": count,
|
|
109
|
+
"query_info": {"applied_preferences": {"external_fallback": False}},
|
|
110
|
+
"page": 2,
|
|
111
|
+
"next_page": 3 if count else None,
|
|
112
|
+
"rich_blocks": {"infobox": {"title": "Coffee"}},
|
|
113
|
+
}
|
|
114
|
+
monkeypatch.setattr(brave, "api_get", Mock(return_value=payload))
|
|
115
|
+
result = CliRunner().invoke(brave.app, ["search", "coffee", "--page", "2"])
|
|
116
|
+
assert result.exit_code == 0, result.output
|
|
117
|
+
assert json.loads(result.stdout) == payload
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Offline GitHub profile request and raw-output contract."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from unittest.mock import Mock
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
from scrapebadger_cli.commands import github
|
|
8
|
+
from scrapebadger_cli.main import app
|
|
9
|
+
from typer.testing import CliRunner
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.mark.parametrize(
|
|
13
|
+
("options", "expected"),
|
|
14
|
+
[
|
|
15
|
+
([], {"tab": "overview"}),
|
|
16
|
+
(["--year", "2024"], {"tab": "overview", "year": 2024}),
|
|
17
|
+
(["--tab", "repositories", "--page", "2"], {"tab": "repositories", "page": 2}),
|
|
18
|
+
(
|
|
19
|
+
["--tab", "stars", "--next-page-token", "a+/=&?雪"],
|
|
20
|
+
{"tab": "stars", "next_page_token": "a+/=&?雪"},
|
|
21
|
+
),
|
|
22
|
+
],
|
|
23
|
+
)
|
|
24
|
+
def test_registered_profile_forwards_inputs_and_preserves_output(monkeypatch, options, expected):
|
|
25
|
+
payload = {"profile": {"login": "octocat"}, "new_source_field": {"count": 0}}
|
|
26
|
+
call, render = Mock(return_value=payload), Mock()
|
|
27
|
+
monkeypatch.setattr(github, "api_get", call)
|
|
28
|
+
monkeypatch.setattr(github, "render", render)
|
|
29
|
+
result = CliRunner().invoke(
|
|
30
|
+
app, ["github", "profile", "@octocat", *options, "--output", "json", "--fields", "profile"]
|
|
31
|
+
)
|
|
32
|
+
assert result.exit_code == 0, result.output
|
|
33
|
+
call.assert_called_once_with("/v1/github/profiles/%40octocat", expected)
|
|
34
|
+
render.assert_called_once_with(payload, "json", "profile", preserve_envelope=True)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_username_cannot_change_request_path(monkeypatch):
|
|
38
|
+
call = Mock(return_value={})
|
|
39
|
+
monkeypatch.setattr(github, "api_get", call)
|
|
40
|
+
monkeypatch.setattr(github, "render", Mock())
|
|
41
|
+
result = CliRunner().invoke(app, ["github", "profile", "user/a?b&c"])
|
|
42
|
+
assert result.exit_code == 0, result.output
|
|
43
|
+
# The API rejects malformed usernames; the adapter must still keep one segment.
|
|
44
|
+
assert call.call_args.args[0] == "/v1/github/profiles/user%2Fa%3Fb%26c"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@pytest.mark.parametrize("options", [["--tab", "search"], ["--tab", "repositories", "--page", "0"]])
|
|
48
|
+
def test_invalid_cli_options_do_not_call_api(monkeypatch, options):
|
|
49
|
+
call = Mock()
|
|
50
|
+
monkeypatch.setattr(github, "api_get", call)
|
|
51
|
+
result = CliRunner().invoke(app, ["github", "profile", "octocat", *options])
|
|
52
|
+
assert result.exit_code != 0
|
|
53
|
+
call.assert_not_called()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@pytest.mark.parametrize("count", [0, 1, 2])
|
|
57
|
+
def test_json_profile_output_preserves_card_rows_and_pagination(monkeypatch, count):
|
|
58
|
+
payload = {
|
|
59
|
+
"profile": {"login": "octocat", "is_organization": False},
|
|
60
|
+
"repositories": [{"full_name": f"octocat/repo{i}"} for i in range(count)],
|
|
61
|
+
"pagination": {"page": 2, "has_next": True, "next_page": 3},
|
|
62
|
+
"additive_source_metadata": {"exact": True},
|
|
63
|
+
}
|
|
64
|
+
monkeypatch.setattr(github, "api_get", Mock(return_value=payload))
|
|
65
|
+
result = CliRunner().invoke(
|
|
66
|
+
app, ["github", "profile", "octocat", "--tab", "repositories", "--page", "2"]
|
|
67
|
+
)
|
|
68
|
+
assert result.exit_code == 0, result.output
|
|
69
|
+
assert json.loads(result.output) == payload
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/aliexpress.py
RENAMED
|
File without changes
|
|
File without changes
|
{scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/apartments.py
RENAMED
|
File without changes
|
{scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/app_store.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/bestbuy.py
RENAMED
|
File without changes
|
|
File without changes
|
{scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/booking.py
RENAMED
|
File without changes
|
{scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/chatgpt.py
RENAMED
|
File without changes
|
{scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/copilot.py
RENAMED
|
File without changes
|
|
File without changes
|
{scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/duckduckgo.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/facebook.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/google_play.py
RENAMED
|
File without changes
|
{scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/gumtree.py
RENAMED
|
File without changes
|
{scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/idealista.py
RENAMED
|
File without changes
|
{scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/immobiliare.py
RENAMED
|
File without changes
|
|
File without changes
|
{scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/instagram.py
RENAMED
|
File without changes
|
{scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/leboncoin.py
RENAMED
|
File without changes
|
{scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/linkedin.py
RENAMED
|
File without changes
|
{scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/loopnet.py
RENAMED
|
File without changes
|
|
File without changes
|
{scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/onboarding.py
RENAMED
|
File without changes
|
{scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/perplexity.py
RENAMED
|
File without changes
|
{scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/realtor.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/twitter.py
RENAMED
|
File without changes
|
|
File without changes
|
{scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/wallapop.py
RENAMED
|
File without changes
|
{scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/walmart.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/youtube.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|