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.
Files changed (75) hide show
  1. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/PKG-INFO +46 -1
  2. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/README.md +45 -0
  3. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/pyproject.toml +1 -1
  4. scrapebadger_cli-0.27.0/src/scrapebadger_cli/commands/autoscout24.py +88 -0
  5. scrapebadger_cli-0.27.0/src/scrapebadger_cli/commands/brave.py +163 -0
  6. scrapebadger_cli-0.27.0/src/scrapebadger_cli/commands/github.py +40 -0
  7. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/main.py +6 -0
  8. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/output.py +12 -1
  9. scrapebadger_cli-0.27.0/tests/conftest.py +6 -0
  10. scrapebadger_cli-0.27.0/tests/test_autoscout24_commands.py +76 -0
  11. scrapebadger_cli-0.27.0/tests/test_brave.py +117 -0
  12. scrapebadger_cli-0.27.0/tests/test_github.py +69 -0
  13. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/.gitignore +0 -0
  14. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/__init__.py +0 -0
  15. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/client.py +0 -0
  16. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/__init__.py +0 -0
  17. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/agent.py +0 -0
  18. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/airbnb.py +0 -0
  19. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/aliexpress.py +0 -0
  20. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/amazon.py +0 -0
  21. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/apartments.py +0 -0
  22. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/app_store.py +0 -0
  23. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/asos.py +0 -0
  24. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/auth.py +0 -0
  25. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/baidu.py +0 -0
  26. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/bestbuy.py +0 -0
  27. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/bing.py +0 -0
  28. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/booking.py +0 -0
  29. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/chatgpt.py +0 -0
  30. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/copilot.py +0 -0
  31. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/depop.py +0 -0
  32. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/duckduckgo.py +0 -0
  33. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/ebay.py +0 -0
  34. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/etsy.py +0 -0
  35. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/facebook.py +0 -0
  36. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/gemini.py +0 -0
  37. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/google.py +0 -0
  38. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/google_play.py +0 -0
  39. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/gumtree.py +0 -0
  40. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/idealista.py +0 -0
  41. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/immobiliare.py +0 -0
  42. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/init.py +0 -0
  43. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/instagram.py +0 -0
  44. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/leboncoin.py +0 -0
  45. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/linkedin.py +0 -0
  46. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/loopnet.py +0 -0
  47. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/naver.py +0 -0
  48. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/onboarding.py +0 -0
  49. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/perplexity.py +0 -0
  50. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/realtor.py +0 -0
  51. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/reddit.py +0 -0
  52. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/redfin.py +0 -0
  53. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/stream.py +0 -0
  54. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/target.py +0 -0
  55. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/tiktok.py +0 -0
  56. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/twitter.py +0 -0
  57. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/vinted.py +0 -0
  58. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/wallapop.py +0 -0
  59. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/walmart.py +0 -0
  60. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/web.py +0 -0
  61. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/yahoo.py +0 -0
  62. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/yandex.py +0 -0
  63. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/youtube.py +0 -0
  64. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/commands/zillow.py +0 -0
  65. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/config.py +0 -0
  66. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/results.py +0 -0
  67. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/src/scrapebadger_cli/views.py +0 -0
  68. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/tests/test_agent_features.py +0 -0
  69. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/tests/test_copilot_commands.py +0 -0
  70. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/tests/test_google_light_search.py +0 -0
  71. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/tests/test_naver.py +0 -0
  72. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/tests/test_perplexity_strict_country.py +0 -0
  73. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/tests/test_tiktok_pagination.py +0 -0
  74. {scrapebadger_cli-0.26.0 → scrapebadger_cli-0.27.0}/tests/test_vinted_image_search.py +0 -0
  75. {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.26.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.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "scrapebadger-cli"
3
- version = "0.26.0"
3
+ version = "0.27.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,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(data: Any, fmt: str = "json", fields: str | None = None) -> None:
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,6 @@
1
+ """Collect CLI regressions from the monorepo without requiring an editable install."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
@@ -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