scrapebadger-cli 0.7.0__tar.gz → 0.9.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 (40) hide show
  1. {scrapebadger_cli-0.7.0 → scrapebadger_cli-0.9.0}/PKG-INFO +1 -1
  2. {scrapebadger_cli-0.7.0 → scrapebadger_cli-0.9.0}/pyproject.toml +1 -1
  3. scrapebadger_cli-0.9.0/src/scrapebadger_cli/commands/baidu.py +95 -0
  4. scrapebadger_cli-0.9.0/src/scrapebadger_cli/commands/bing.py +126 -0
  5. scrapebadger_cli-0.9.0/src/scrapebadger_cli/commands/duckduckgo.py +176 -0
  6. {scrapebadger_cli-0.7.0 → scrapebadger_cli-0.9.0}/src/scrapebadger_cli/commands/facebook.py +0 -60
  7. {scrapebadger_cli-0.7.0 → scrapebadger_cli-0.9.0}/src/scrapebadger_cli/commands/google.py +56 -0
  8. {scrapebadger_cli-0.7.0 → scrapebadger_cli-0.9.0}/src/scrapebadger_cli/commands/instagram.py +28 -28
  9. scrapebadger_cli-0.9.0/src/scrapebadger_cli/commands/walmart.py +211 -0
  10. scrapebadger_cli-0.9.0/src/scrapebadger_cli/commands/yandex.py +102 -0
  11. {scrapebadger_cli-0.7.0 → scrapebadger_cli-0.9.0}/src/scrapebadger_cli/main.py +10 -0
  12. {scrapebadger_cli-0.7.0 → scrapebadger_cli-0.9.0}/uv.lock +1 -1
  13. {scrapebadger_cli-0.7.0 → scrapebadger_cli-0.9.0}/.gitignore +0 -0
  14. {scrapebadger_cli-0.7.0 → scrapebadger_cli-0.9.0}/README.md +0 -0
  15. {scrapebadger_cli-0.7.0 → scrapebadger_cli-0.9.0}/src/scrapebadger_cli/__init__.py +0 -0
  16. {scrapebadger_cli-0.7.0 → scrapebadger_cli-0.9.0}/src/scrapebadger_cli/client.py +0 -0
  17. {scrapebadger_cli-0.7.0 → scrapebadger_cli-0.9.0}/src/scrapebadger_cli/commands/__init__.py +0 -0
  18. {scrapebadger_cli-0.7.0 → scrapebadger_cli-0.9.0}/src/scrapebadger_cli/commands/amazon.py +0 -0
  19. {scrapebadger_cli-0.7.0 → scrapebadger_cli-0.9.0}/src/scrapebadger_cli/commands/apartments.py +0 -0
  20. {scrapebadger_cli-0.7.0 → scrapebadger_cli-0.9.0}/src/scrapebadger_cli/commands/auth.py +0 -0
  21. {scrapebadger_cli-0.7.0 → scrapebadger_cli-0.9.0}/src/scrapebadger_cli/commands/chatgpt.py +0 -0
  22. {scrapebadger_cli-0.7.0 → scrapebadger_cli-0.9.0}/src/scrapebadger_cli/commands/depop.py +0 -0
  23. {scrapebadger_cli-0.7.0 → scrapebadger_cli-0.9.0}/src/scrapebadger_cli/commands/ebay.py +0 -0
  24. {scrapebadger_cli-0.7.0 → scrapebadger_cli-0.9.0}/src/scrapebadger_cli/commands/idealista.py +0 -0
  25. {scrapebadger_cli-0.7.0 → scrapebadger_cli-0.9.0}/src/scrapebadger_cli/commands/immobiliare.py +0 -0
  26. {scrapebadger_cli-0.7.0 → scrapebadger_cli-0.9.0}/src/scrapebadger_cli/commands/leboncoin.py +0 -0
  27. {scrapebadger_cli-0.7.0 → scrapebadger_cli-0.9.0}/src/scrapebadger_cli/commands/linkedin.py +0 -0
  28. {scrapebadger_cli-0.7.0 → scrapebadger_cli-0.9.0}/src/scrapebadger_cli/commands/loopnet.py +0 -0
  29. {scrapebadger_cli-0.7.0 → scrapebadger_cli-0.9.0}/src/scrapebadger_cli/commands/realtor.py +0 -0
  30. {scrapebadger_cli-0.7.0 → scrapebadger_cli-0.9.0}/src/scrapebadger_cli/commands/reddit.py +0 -0
  31. {scrapebadger_cli-0.7.0 → scrapebadger_cli-0.9.0}/src/scrapebadger_cli/commands/redfin.py +0 -0
  32. {scrapebadger_cli-0.7.0 → scrapebadger_cli-0.9.0}/src/scrapebadger_cli/commands/stream.py +0 -0
  33. {scrapebadger_cli-0.7.0 → scrapebadger_cli-0.9.0}/src/scrapebadger_cli/commands/tiktok.py +0 -0
  34. {scrapebadger_cli-0.7.0 → scrapebadger_cli-0.9.0}/src/scrapebadger_cli/commands/twitter.py +0 -0
  35. {scrapebadger_cli-0.7.0 → scrapebadger_cli-0.9.0}/src/scrapebadger_cli/commands/vinted.py +0 -0
  36. {scrapebadger_cli-0.7.0 → scrapebadger_cli-0.9.0}/src/scrapebadger_cli/commands/web.py +0 -0
  37. {scrapebadger_cli-0.7.0 → scrapebadger_cli-0.9.0}/src/scrapebadger_cli/commands/youtube.py +0 -0
  38. {scrapebadger_cli-0.7.0 → scrapebadger_cli-0.9.0}/src/scrapebadger_cli/commands/zillow.py +0 -0
  39. {scrapebadger_cli-0.7.0 → scrapebadger_cli-0.9.0}/src/scrapebadger_cli/config.py +0 -0
  40. {scrapebadger_cli-0.7.0 → scrapebadger_cli-0.9.0}/src/scrapebadger_cli/output.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scrapebadger-cli
3
- Version: 0.7.0
3
+ Version: 0.9.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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "scrapebadger-cli"
3
- version = "0.7.0"
3
+ version = "0.9.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,95 @@
1
+ """Baidu search engine 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
+
10
+ FMT = typer.Option("json", "--output", "-o", help="Output format: json, csv, table, markdown")
11
+ FIELDS = typer.Option(None, "--fields", "-f", help="Comma-separated fields to include")
12
+
13
+ _RESULT_COLS = "position,title,url,source,date"
14
+
15
+
16
+ def _show(
17
+ data: object, fmt: str, fields: str | None, table_cols: str, key: str | None = None
18
+ ) -> None:
19
+ """Render, defaulting the columns for the human formats only.
20
+
21
+ json and csv keep the whole envelope so a machine-readable export never
22
+ silently drops fields; table/markdown drill to `key` (the response's own
23
+ row list, when `output.render` cannot find it) and default to `table_cols`.
24
+ """
25
+ if fmt in ("table", "markdown"):
26
+ if key and isinstance(data, dict):
27
+ data = data.get(key, data)
28
+ fields = fields or table_cols
29
+ render(data, fmt, fields)
30
+
31
+
32
+ @app.command()
33
+ def search(
34
+ query: str = typer.Argument(..., help="Search keywords, e.g. '咖啡机'"),
35
+ page: int = typer.Option(1, "--page", "-p", help="1-76"),
36
+ num: int = typer.Option(10, "--num", "-n", help="Results per page, 1-50"),
37
+ language: str | None = typer.Option(
38
+ None, "--language", "-l", help="all, zh-cn (simplified), zh-tw (traditional)"
39
+ ),
40
+ time_from: int | None = typer.Option(
41
+ None, "--time-from", help="Unix timestamp — published after"
42
+ ),
43
+ time_to: int | None = typer.Option(None, "--time-to", help="Unix timestamp — published before"),
44
+ fmt: str = FMT,
45
+ fields: str | None = FIELDS,
46
+ ) -> None:
47
+ """Search baidu.com — organic results with the real target URL."""
48
+ data = api_get(
49
+ "/v1/baidu/search",
50
+ {
51
+ "query": query,
52
+ "page": page,
53
+ "num": num,
54
+ "language": language,
55
+ "time_from": time_from,
56
+ "time_to": time_to,
57
+ },
58
+ )
59
+ _show(data, fmt, fields, _RESULT_COLS, key="results")
60
+
61
+
62
+ @app.command()
63
+ def news(
64
+ query: str = typer.Argument(..., help="Search keywords"),
65
+ page: int = typer.Option(1, "--page", "-p", help="1-76"),
66
+ sort: str | None = typer.Option(None, "--sort", "-s", help="relevance or time"),
67
+ fmt: str = FMT,
68
+ fields: str | None = FIELDS,
69
+ ) -> None:
70
+ """Search the Baidu news vertical."""
71
+ data = api_get("/v1/baidu/news", {"query": query, "page": page, "sort": sort})
72
+ _show(data, fmt, fields, _RESULT_COLS, key="results")
73
+
74
+
75
+ @app.command()
76
+ def images(
77
+ query: str = typer.Argument(..., help="Search keywords"),
78
+ page: int = typer.Option(1, "--page", "-p", help="1-50 (30 images per page)"),
79
+ fmt: str = FMT,
80
+ fields: str | None = FIELDS,
81
+ ) -> None:
82
+ """Search Baidu images."""
83
+ data = api_get("/v1/baidu/images", {"query": query, "page": page})
84
+ _show(data, fmt, fields, "position,title,image_url,width,height,from_url", key="results")
85
+
86
+
87
+ @app.command()
88
+ def autocomplete(
89
+ query: str = typer.Argument(..., help="Partial search term, e.g. '咖啡'"),
90
+ fmt: str = FMT,
91
+ fields: str | None = FIELDS,
92
+ ) -> None:
93
+ """Get Baidu search-box suggestions."""
94
+ data = api_get("/v1/baidu/autocomplete", {"query": query})
95
+ _show(data, fmt, fields, "query,type", key="suggestions")
@@ -0,0 +1,126 @@
1
+ """Bing search engine commands. market is a language-COUNTRY code, e.g. en-US."""
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 format: json, csv, table, markdown")
11
+ FIELDS = typer.Option(None, "--fields", "-f", help="Comma-separated fields to include")
12
+
13
+ MARKET = typer.Option("en-US", "--market", "-m", help="Bing market code, e.g. en-US")
14
+ SAFE_HELP = "off, moderate, strict"
15
+
16
+
17
+ def _show(
18
+ data: object, fmt: str, fields: str | None, table_cols: str, key: str | None = None
19
+ ) -> None:
20
+ """Render, defaulting the columns for the human formats only.
21
+
22
+ json and csv keep the whole envelope so a machine-readable export never
23
+ silently drops fields; table/markdown drill to `key` (the response's own
24
+ row list, when `output.render` cannot find it) and default to `table_cols`.
25
+ """
26
+ if fmt in ("table", "markdown"):
27
+ if key and isinstance(data, dict):
28
+ data = data.get(key, data)
29
+ fields = fields or table_cols
30
+ render(data, fmt, fields)
31
+
32
+
33
+ @app.command()
34
+ def search(
35
+ query: str = typer.Argument(..., help="Search keywords, e.g. 'coffee machine'"),
36
+ market: str = MARKET,
37
+ count: int = typer.Option(10, "--count", "-c", help="Results per page, 1-50"),
38
+ offset: int = typer.Option(0, "--offset", help="Zero-based result offset"),
39
+ safe_search: str | None = typer.Option(None, "--safe-search", help=SAFE_HELP),
40
+ fmt: str = FMT,
41
+ fields: str | None = FIELDS,
42
+ ) -> None:
43
+ """Search the Bing web SERP: organic results, ads and related searches."""
44
+ data = api_get(
45
+ "/v1/bing/search",
46
+ {
47
+ "query": query,
48
+ "market": market,
49
+ "count": count,
50
+ "offset": offset,
51
+ "safe_search": safe_search,
52
+ },
53
+ )
54
+ _show(data, fmt, fields, "position,title,display_url,snippet")
55
+
56
+
57
+ @app.command()
58
+ def images(
59
+ query: str = typer.Argument(..., help="Search keywords, e.g. 'golden retriever'"),
60
+ market: str = MARKET,
61
+ count: int = typer.Option(35, "--count", "-c", help="Results to return"),
62
+ safe_search: str | None = typer.Option(None, "--safe-search", help=SAFE_HELP),
63
+ fmt: str = FMT,
64
+ fields: str | None = FIELDS,
65
+ ) -> None:
66
+ """Search Bing Images: full-size URL, thumbnail and source page."""
67
+ data = api_get(
68
+ "/v1/bing/images",
69
+ {"query": query, "market": market, "count": count, "safe_search": safe_search},
70
+ )
71
+ _show(data, fmt, fields, "position,title,image_url,source_domain,width,height")
72
+
73
+
74
+ @app.command()
75
+ def videos(
76
+ query: str = typer.Argument(..., help="Search keywords, e.g. 'espresso tutorial'"),
77
+ market: str = MARKET,
78
+ count: int = typer.Option(35, "--count", "-c", help="Results to return"),
79
+ safe_search: str | None = typer.Option(None, "--safe-search", help=SAFE_HELP),
80
+ fmt: str = FMT,
81
+ fields: str | None = FIELDS,
82
+ ) -> None:
83
+ """Search Bing Videos: title, duration, publisher and source platform."""
84
+ data = api_get(
85
+ "/v1/bing/videos",
86
+ {"query": query, "market": market, "count": count, "safe_search": safe_search},
87
+ )
88
+ _show(data, fmt, fields, "position,title,duration,source,publisher,views,published")
89
+
90
+
91
+ @app.command()
92
+ def news(
93
+ query: str = typer.Argument(..., help="Search keywords, e.g. 'interest rates'"),
94
+ market: str = MARKET,
95
+ freshness: str | None = typer.Option(None, "--freshness", help="day, week, month"),
96
+ fmt: str = FMT,
97
+ fields: str | None = FIELDS,
98
+ ) -> None:
99
+ """Search Bing News: headline, source and absolute publish time."""
100
+ data = api_get(
101
+ "/v1/bing/news",
102
+ {"query": query, "market": market, "freshness": freshness},
103
+ )
104
+ _show(data, fmt, fields, "position,title,source,published_at")
105
+
106
+
107
+ @app.command()
108
+ def autocomplete(
109
+ query: str = typer.Argument(..., help="Partial search term, e.g. 'coff'"),
110
+ market: str = MARKET,
111
+ fmt: str = FMT,
112
+ fields: str | None = FIELDS,
113
+ ) -> None:
114
+ """Get Bing search-box query suggestions."""
115
+ data = api_get("/v1/bing/autocomplete", {"query": query, "market": market})
116
+ _show(data, fmt, fields, "value", key="suggestions")
117
+
118
+
119
+ @app.command()
120
+ def markets(
121
+ fmt: str = FMT,
122
+ fields: str | None = FIELDS,
123
+ ) -> None:
124
+ """List the curated Bing market codes (30+). Free."""
125
+ data = api_get("/v1/bing/markets")
126
+ _show(data, fmt, fields, "code,name,country", key="markets")
@@ -0,0 +1,176 @@
1
+ """DuckDuckGo search commands. region is a DuckDuckGo kl code (wt-wt = all regions)."""
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 format: json, csv, table, markdown")
11
+ FIELDS = typer.Option(None, "--fields", "-f", help="Comma-separated fields to include")
12
+
13
+ REGION = typer.Option(
14
+ "wt-wt", "--region", "-r", help="DuckDuckGo region code (kl), e.g. us-en. wt-wt = all regions"
15
+ )
16
+ SAFESEARCH = typer.Option("moderate", "--safesearch", help="on, moderate, off")
17
+ TIMELIMIT = typer.Option(None, "--timelimit", "-t", help="day, week, month, year")
18
+
19
+
20
+ def _show(
21
+ data: object, fmt: str, fields: str | None, table_cols: str, key: str | None = None
22
+ ) -> None:
23
+ """Render, defaulting the columns for the human formats only.
24
+
25
+ json and csv keep the whole envelope so a machine-readable export never
26
+ silently drops fields; table/markdown drill to `key` (the response's own
27
+ row list, when `output.render` cannot find it) and default to `table_cols`.
28
+ """
29
+ if fmt in ("table", "markdown"):
30
+ if key and isinstance(data, dict):
31
+ data = data.get(key, data)
32
+ fields = fields or table_cols
33
+ render(data, fmt, fields)
34
+
35
+
36
+ @app.command()
37
+ def search(
38
+ query: str = typer.Argument(..., help="Search query, e.g. 'web scraping'"),
39
+ region: str = REGION,
40
+ safesearch: str = SAFESEARCH,
41
+ timelimit: str | None = TIMELIMIT,
42
+ page: int = typer.Option(1, "--page", "-p", help="1-25"),
43
+ fmt: str = FMT,
44
+ fields: str | None = FIELDS,
45
+ ) -> None:
46
+ """Search the DuckDuckGo web SERP (organic results, ads flagged is_ad)."""
47
+ data = api_get(
48
+ "/v1/duckduckgo/search",
49
+ {
50
+ "query": query,
51
+ "region": region,
52
+ "safesearch": safesearch,
53
+ "timelimit": timelimit,
54
+ "page": page,
55
+ },
56
+ )
57
+ _show(data, fmt, fields, "title,url,display_url,is_ad", key="results")
58
+
59
+
60
+ @app.command()
61
+ def images(
62
+ query: str = typer.Argument(..., help="Search query, e.g. 'sunset'"),
63
+ region: str = REGION,
64
+ safesearch: str = SAFESEARCH,
65
+ page: int = typer.Option(1, "--page", "-p", help="1-10 (100 results per page)"),
66
+ size: str | None = typer.Option(None, "--size", help="Small, Medium, Large, Wallpaper"),
67
+ color: str | None = typer.Option(None, "--color", help="color, Monochrome, Red, Blue, …"),
68
+ image_type: str | None = typer.Option(
69
+ None, "--type", help="photo, clipart, gif, transparent, line"
70
+ ),
71
+ layout: str | None = typer.Option(None, "--layout", help="Square, Tall, Wide"),
72
+ license: str | None = typer.Option(
73
+ None, "--license", help="Any, Public, Share, ShareCommercially, Modify"
74
+ ),
75
+ fmt: str = FMT,
76
+ fields: str | None = FIELDS,
77
+ ) -> None:
78
+ """Search DuckDuckGo images with size/color/type/layout/license filters."""
79
+ data = api_get(
80
+ "/v1/duckduckgo/images",
81
+ {
82
+ "query": query,
83
+ "region": region,
84
+ "safesearch": safesearch,
85
+ "page": page,
86
+ "size": size,
87
+ "color": color,
88
+ "image_type": image_type,
89
+ "layout": layout,
90
+ "license": license,
91
+ },
92
+ )
93
+ _show(data, fmt, fields, "title,image,width,height,source", key="results")
94
+
95
+
96
+ @app.command()
97
+ def news(
98
+ query: str = typer.Argument(..., help="Search query, e.g. 'artificial intelligence'"),
99
+ region: str = REGION,
100
+ safesearch: str = SAFESEARCH,
101
+ timelimit: str | None = TIMELIMIT,
102
+ page: int = typer.Option(1, "--page", "-p", help="1-10 (30 results per page)"),
103
+ fmt: str = FMT,
104
+ fields: str | None = FIELDS,
105
+ ) -> None:
106
+ """Search DuckDuckGo news (headline, source, excerpt, dates, image)."""
107
+ data = api_get(
108
+ "/v1/duckduckgo/news",
109
+ {
110
+ "query": query,
111
+ "region": region,
112
+ "safesearch": safesearch,
113
+ "timelimit": timelimit,
114
+ "page": page,
115
+ },
116
+ )
117
+ _show(data, fmt, fields, "title,source,relative_time,url", key="results")
118
+
119
+
120
+ @app.command()
121
+ def videos(
122
+ query: str = typer.Argument(..., help="Search query, e.g. 'python tutorial'"),
123
+ region: str = REGION,
124
+ safesearch: str = SAFESEARCH,
125
+ page: int = typer.Option(1, "--page", "-p", help="1-10 (60 results per page)"),
126
+ duration: str | None = typer.Option(None, "--duration", help="short, medium, long"),
127
+ resolution: str | None = typer.Option(None, "--resolution", help="high, standard"),
128
+ fmt: str = FMT,
129
+ fields: str | None = FIELDS,
130
+ ) -> None:
131
+ """Search DuckDuckGo videos (title, publisher, duration, views)."""
132
+ data = api_get(
133
+ "/v1/duckduckgo/videos",
134
+ {
135
+ "query": query,
136
+ "region": region,
137
+ "safesearch": safesearch,
138
+ "page": page,
139
+ "duration": duration,
140
+ "resolution": resolution,
141
+ },
142
+ )
143
+ _show(data, fmt, fields, "title,publisher,duration,view_count,content", key="results")
144
+
145
+
146
+ @app.command()
147
+ def autocomplete(
148
+ query: str = typer.Argument(..., help="Partial query, e.g. 'web scra'"),
149
+ region: str = REGION,
150
+ fmt: str = FMT,
151
+ fields: str | None = FIELDS,
152
+ ) -> None:
153
+ """Get DuckDuckGo search-box suggestions."""
154
+ data = api_get("/v1/duckduckgo/autocomplete", {"query": query, "region": region})
155
+ _show(data, fmt, fields, "suggestions", key="suggestions")
156
+
157
+
158
+ @app.command()
159
+ def instant(
160
+ query: str = typer.Argument(..., help="Query for the Instant Answer API, e.g. 'Nikola Tesla'"),
161
+ fmt: str = FMT,
162
+ fields: str | None = FIELDS,
163
+ ) -> None:
164
+ """Query the DuckDuckGo Instant Answer API (abstract, answer, definition)."""
165
+ data = api_get("/v1/duckduckgo/instant", {"query": query})
166
+ _show(data, fmt, fields, "heading,abstract_text,abstract_source,answer,type")
167
+
168
+
169
+ @app.command()
170
+ def regions(
171
+ fmt: str = FMT,
172
+ fields: str | None = FIELDS,
173
+ ) -> None:
174
+ """List all supported DuckDuckGo region (kl) codes."""
175
+ data = api_get("/v1/duckduckgo/regions")
176
+ _show(data, fmt, fields, "code,name", key="regions")
@@ -109,66 +109,6 @@ def categories(
109
109
  render(data, fmt, fields)
110
110
 
111
111
 
112
- @app.command()
113
- def search(
114
- q: str = typer.Argument(..., help="Search query"),
115
- after: str | None = AFTER,
116
- fmt: str = FMT,
117
- fields: str | None = FIELDS,
118
- ) -> None:
119
- """Search Facebook globally (pages, people, groups, posts)."""
120
- data = api_get("/v1/facebook/search", {"q": q, "after": after})
121
- render(data, fmt, fields)
122
-
123
-
124
- @app.command(name="search-pages")
125
- def search_pages(
126
- q: str = typer.Argument(..., help="Search query"),
127
- after: str | None = AFTER,
128
- fmt: str = FMT,
129
- fields: str | None = FIELDS,
130
- ) -> None:
131
- """Search Facebook Pages."""
132
- data = api_get("/v1/facebook/search/pages", {"q": q, "after": after})
133
- render(data, fmt, fields)
134
-
135
-
136
- @app.command(name="search-people")
137
- def search_people(
138
- q: str = typer.Argument(..., help="Search query"),
139
- after: str | None = AFTER,
140
- fmt: str = FMT,
141
- fields: str | None = FIELDS,
142
- ) -> None:
143
- """Search Facebook profiles."""
144
- data = api_get("/v1/facebook/search/people", {"q": q, "after": after})
145
- render(data, fmt, fields)
146
-
147
-
148
- @app.command(name="search-groups")
149
- def search_groups(
150
- q: str = typer.Argument(..., help="Search query"),
151
- after: str | None = AFTER,
152
- fmt: str = FMT,
153
- fields: str | None = FIELDS,
154
- ) -> None:
155
- """Search Facebook groups."""
156
- data = api_get("/v1/facebook/search/groups", {"q": q, "after": after})
157
- render(data, fmt, fields)
158
-
159
-
160
- @app.command(name="search-posts")
161
- def search_posts(
162
- q: str = typer.Argument(..., help="Search query"),
163
- after: str | None = AFTER,
164
- fmt: str = FMT,
165
- fields: str | None = FIELDS,
166
- ) -> None:
167
- """Search public Facebook posts."""
168
- data = api_get("/v1/facebook/search/posts", {"q": q, "after": after})
169
- render(data, fmt, fields)
170
-
171
-
172
112
  @app.command()
173
113
  def page(
174
114
  identifier: str = typer.Argument(..., help="Page username or numeric page ID"),
@@ -1123,3 +1123,59 @@ def flights_search(
1123
1123
  },
1124
1124
  )
1125
1125
  render(data, fmt, fields)
1126
+
1127
+
1128
+ @app.command(name="flights-calendar")
1129
+ def flights_calendar(
1130
+ departure_id: str = typer.Argument(..., help="Departure airport IATA (e.g. JFK)"),
1131
+ arrival_id: str = typer.Argument(..., help="Arrival airport IATA (e.g. LHR)"),
1132
+ outbound_date_from: str = typer.Argument(..., help="First outbound date (YYYY-MM-DD)"),
1133
+ outbound_date_to: str = typer.Argument(
1134
+ ..., help="Last outbound date (YYYY-MM-DD); <=200 days, or <=14 in date-grid mode"
1135
+ ),
1136
+ trip_type: str | None = typer.Option(
1137
+ None, "--trip-type", help="one_way (default) | round_trip"
1138
+ ),
1139
+ trip_length_days: int | None = typer.Option(
1140
+ None, "--trip-length-days", help="Round-trip stay length in nights (default 7)"
1141
+ ),
1142
+ return_date_from: str | None = typer.Option(
1143
+ None,
1144
+ "--return-date-from",
1145
+ help="Date-grid mode: first return date (with --return-date-to, <=14 days)",
1146
+ ),
1147
+ return_date_to: str | None = typer.Option(
1148
+ None, "--return-date-to", help="Date-grid mode: last return date"
1149
+ ),
1150
+ adults: int | None = typer.Option(None, "--adults", help="Adult passengers (1-9)"),
1151
+ children: int | None = typer.Option(None, "--children", help="Children passengers (0-8)"),
1152
+ travel_class: str | None = typer.Option(
1153
+ None, "--travel-class", help="economy | premium_economy | business | first"
1154
+ ),
1155
+ currency: str | None = typer.Option(None, "--currency", help="ISO-4217 currency code"),
1156
+ gl: str | None = typer.Option(None, "--gl", help="Country code"),
1157
+ hl: str | None = typer.Option(None, "--hl", help="Language code"),
1158
+ fmt: str = FMT,
1159
+ fields: str | None = FIELDS,
1160
+ ) -> None:
1161
+ """Cheapest fare per date — up to 200 dates in one call (price graph / date grid)."""
1162
+ data = api_get(
1163
+ "/v1/google/flights/calendar",
1164
+ {
1165
+ "departure_id": departure_id,
1166
+ "arrival_id": arrival_id,
1167
+ "outbound_date_from": outbound_date_from,
1168
+ "outbound_date_to": outbound_date_to,
1169
+ "trip_type": trip_type,
1170
+ "trip_length_days": trip_length_days,
1171
+ "return_date_from": return_date_from,
1172
+ "return_date_to": return_date_to,
1173
+ "adults": adults,
1174
+ "children": children,
1175
+ "travel_class": travel_class,
1176
+ "currency": currency,
1177
+ "gl": gl,
1178
+ "hl": hl,
1179
+ },
1180
+ )
1181
+ render(data, fmt, fields)
@@ -33,7 +33,7 @@ def about(
33
33
  fmt: str = FMT,
34
34
  fields: str | None = FIELDS,
35
35
  ) -> None:
36
- """Get 'About this account' info."""
36
+ """[temporarily unavailable] Get 'About this account' info."""
37
37
  data = api_get(f"/v1/instagram/users/{username}/about")
38
38
  render(data, fmt, fields)
39
39
 
@@ -73,7 +73,7 @@ def videos(
73
73
  fmt: str = FMT,
74
74
  fields: str | None = FIELDS,
75
75
  ) -> None:
76
- """Get a user's videos."""
76
+ """[temporarily unavailable] Get a user's videos."""
77
77
  data = api_get(f"/v1/instagram/users/{username}/videos", {"amount": amount, "cursor": cursor})
78
78
  render(data, fmt, fields)
79
79
 
@@ -86,7 +86,7 @@ def reels(
86
86
  fmt: str = FMT,
87
87
  fields: str | None = FIELDS,
88
88
  ) -> None:
89
- """Get a user's reels."""
89
+ """[temporarily unavailable] Get a user's reels."""
90
90
  data = api_get(f"/v1/instagram/users/{username}/reels", {"amount": amount, "cursor": cursor})
91
91
  render(data, fmt, fields)
92
92
 
@@ -99,7 +99,7 @@ def tagged(
99
99
  fmt: str = FMT,
100
100
  fields: str | None = FIELDS,
101
101
  ) -> None:
102
- """Get media a user is tagged in."""
102
+ """[temporarily unavailable] Get media a user is tagged in."""
103
103
  data = api_get(f"/v1/instagram/users/{username}/tagged", {"amount": amount, "cursor": cursor})
104
104
  render(data, fmt, fields)
105
105
 
@@ -110,7 +110,7 @@ def pinned(
110
110
  fmt: str = FMT,
111
111
  fields: str | None = FIELDS,
112
112
  ) -> None:
113
- """Get a user's pinned posts."""
113
+ """[temporarily unavailable] Get a user's pinned posts."""
114
114
  data = api_get(f"/v1/instagram/users/{username}/pinned")
115
115
  render(data, fmt, fields)
116
116
 
@@ -152,7 +152,7 @@ def followers(
152
152
  fmt: str = FMT,
153
153
  fields: str | None = FIELDS,
154
154
  ) -> None:
155
- """Get a user's followers."""
155
+ """[temporarily unavailable] Get a user's followers."""
156
156
  data = api_get(
157
157
  f"/v1/instagram/users/{username}/followers",
158
158
  {"amount": amount, "cursor": cursor, "order": order},
@@ -168,7 +168,7 @@ def following(
168
168
  fmt: str = FMT,
169
169
  fields: str | None = FIELDS,
170
170
  ) -> None:
171
- """Get accounts a user follows."""
171
+ """[temporarily unavailable] Get accounts a user follows."""
172
172
  data = api_get(
173
173
  f"/v1/instagram/users/{username}/following", {"amount": amount, "cursor": cursor}
174
174
  )
@@ -184,7 +184,7 @@ def search_followers(
184
184
  fmt: str = FMT,
185
185
  fields: str | None = FIELDS,
186
186
  ) -> None:
187
- """Search within a user's followers."""
187
+ """[temporarily unavailable] Search within a user's followers."""
188
188
  data = api_get(
189
189
  f"/v1/instagram/users/{username}/followers/search",
190
190
  {"query": query, "amount": amount, "cursor": cursor},
@@ -201,7 +201,7 @@ def stories(
201
201
  fmt: str = FMT,
202
202
  fields: str | None = FIELDS,
203
203
  ) -> None:
204
- """Get a user's active stories."""
204
+ """[temporarily unavailable] Get a user's active stories."""
205
205
  data = api_get(f"/v1/instagram/users/{username}/stories")
206
206
  render(data, fmt, fields)
207
207
 
@@ -212,7 +212,7 @@ def highlights(
212
212
  fmt: str = FMT,
213
213
  fields: str | None = FIELDS,
214
214
  ) -> None:
215
- """Get a user's highlight reels."""
215
+ """[temporarily unavailable] Get a user's highlight reels."""
216
216
  data = api_get(f"/v1/instagram/users/{username}/highlights")
217
217
  render(data, fmt, fields)
218
218
 
@@ -223,7 +223,7 @@ def story(
223
223
  fmt: str = FMT,
224
224
  fields: str | None = FIELDS,
225
225
  ) -> None:
226
- """Get a single story."""
226
+ """[temporarily unavailable] Get a single story."""
227
227
  data = api_get(f"/v1/instagram/stories/{story_pk}")
228
228
  render(data, fmt, fields)
229
229
 
@@ -234,7 +234,7 @@ def highlight(
234
234
  fmt: str = FMT,
235
235
  fields: str | None = FIELDS,
236
236
  ) -> None:
237
- """Get a highlight reel and its items."""
237
+ """[temporarily unavailable] Get a highlight reel and its items."""
238
238
  data = api_get(f"/v1/instagram/highlights/{highlight_pk}")
239
239
  render(data, fmt, fields)
240
240
 
@@ -264,7 +264,7 @@ def comment_replies(
264
264
  fmt: str = FMT,
265
265
  fields: str | None = FIELDS,
266
266
  ) -> None:
267
- """Get replies to a comment."""
267
+ """[temporarily unavailable] Get replies to a comment."""
268
268
  data = api_get(
269
269
  f"/v1/instagram/media/{code}/comments/{comment_id}/replies",
270
270
  {"amount": amount, "cursor": cursor},
@@ -297,7 +297,7 @@ def likers(
297
297
  fmt: str = FMT,
298
298
  fields: str | None = FIELDS,
299
299
  ) -> None:
300
- """Get users who liked a media item."""
300
+ """[temporarily unavailable] Get users who liked a media item."""
301
301
  data = api_get(f"/v1/instagram/media/{code}/likers", {"amount": amount, "cursor": cursor})
302
302
  render(data, fmt, fields)
303
303
 
@@ -324,7 +324,7 @@ def hashtag_top(
324
324
  fmt: str = FMT,
325
325
  fields: str | None = FIELDS,
326
326
  ) -> None:
327
- """Get top media for a hashtag."""
327
+ """[temporarily unavailable] Get top media for a hashtag."""
328
328
  data = api_get(f"/v1/instagram/hashtags/{tag}/top", {"amount": amount, "cursor": cursor})
329
329
  render(data, fmt, fields)
330
330
 
@@ -350,7 +350,7 @@ def hashtag_reels(
350
350
  fmt: str = FMT,
351
351
  fields: str | None = FIELDS,
352
352
  ) -> None:
353
- """Get reels for a hashtag."""
353
+ """[temporarily unavailable] Get reels for a hashtag."""
354
354
  data = api_get(f"/v1/instagram/hashtags/{tag}/reels", {"amount": amount, "cursor": cursor})
355
355
  render(data, fmt, fields)
356
356
 
@@ -364,7 +364,7 @@ def location(
364
364
  fmt: str = FMT,
365
365
  fields: str | None = FIELDS,
366
366
  ) -> None:
367
- """Get location metadata."""
367
+ """[temporarily unavailable] Get location metadata."""
368
368
  data = api_get(f"/v1/instagram/locations/{location_pk}")
369
369
  render(data, fmt, fields)
370
370
 
@@ -377,7 +377,7 @@ def location_top(
377
377
  fmt: str = FMT,
378
378
  fields: str | None = FIELDS,
379
379
  ) -> None:
380
- """Get top media for a location."""
380
+ """[temporarily unavailable] Get top media for a location."""
381
381
  data = api_get(
382
382
  f"/v1/instagram/locations/{location_pk}/top", {"amount": amount, "cursor": cursor}
383
383
  )
@@ -392,7 +392,7 @@ def location_recent(
392
392
  fmt: str = FMT,
393
393
  fields: str | None = FIELDS,
394
394
  ) -> None:
395
- """Get recent media for a location."""
395
+ """[temporarily unavailable] Get recent media for a location."""
396
396
  data = api_get(
397
397
  f"/v1/instagram/locations/{location_pk}/recent", {"amount": amount, "cursor": cursor}
398
398
  )
@@ -407,7 +407,7 @@ def location_search(
407
407
  fmt: str = FMT,
408
408
  fields: str | None = FIELDS,
409
409
  ) -> None:
410
- """Search locations by name or coordinates."""
410
+ """[temporarily unavailable] Search locations by name or coordinates."""
411
411
  data = api_get("/v1/instagram/locations/search", {"query": query, "lat": lat, "lng": lng})
412
412
  render(data, fmt, fields)
413
413
 
@@ -421,7 +421,7 @@ def search_users(
421
421
  fmt: str = FMT,
422
422
  fields: str | None = FIELDS,
423
423
  ) -> None:
424
- """Search users."""
424
+ """[temporarily unavailable] Search users."""
425
425
  data = api_get("/v1/instagram/search/users", {"query": query})
426
426
  render(data, fmt, fields)
427
427
 
@@ -445,7 +445,7 @@ def search_places(
445
445
  fmt: str = FMT,
446
446
  fields: str | None = FIELDS,
447
447
  ) -> None:
448
- """Search places by name or coordinates."""
448
+ """[temporarily unavailable] Search places by name or coordinates."""
449
449
  data = api_get("/v1/instagram/search/places", {"query": query, "lat": lat, "lng": lng})
450
450
  render(data, fmt, fields)
451
451
 
@@ -467,7 +467,7 @@ def search_reels(
467
467
  fmt: str = FMT,
468
468
  fields: str | None = FIELDS,
469
469
  ) -> None:
470
- """Search reels."""
470
+ """[temporarily unavailable] Search reels."""
471
471
  data = api_get("/v1/instagram/search/reels", {"query": query})
472
472
  render(data, fmt, fields)
473
473
 
@@ -478,7 +478,7 @@ def search_music(
478
478
  fmt: str = FMT,
479
479
  fields: str | None = FIELDS,
480
480
  ) -> None:
481
- """Search music/audio."""
481
+ """[temporarily unavailable] Search music/audio."""
482
482
  data = api_get("/v1/instagram/search/music", {"query": query})
483
483
  render(data, fmt, fields)
484
484
 
@@ -489,7 +489,7 @@ def autocomplete(
489
489
  fmt: str = FMT,
490
490
  fields: str | None = FIELDS,
491
491
  ) -> None:
492
- """Get search autocomplete suggestions."""
492
+ """[temporarily unavailable] Get search autocomplete suggestions."""
493
493
  data = api_get("/v1/instagram/search/autocomplete", {"query": query})
494
494
  render(data, fmt, fields)
495
495
 
@@ -503,7 +503,7 @@ def audio(
503
503
  fmt: str = FMT,
504
504
  fields: str | None = FIELDS,
505
505
  ) -> None:
506
- """Get audio track metadata."""
506
+ """[temporarily unavailable] Get audio track metadata."""
507
507
  data = api_get(f"/v1/instagram/audio/{audio_id}")
508
508
  render(data, fmt, fields)
509
509
 
@@ -516,7 +516,7 @@ def audio_media(
516
516
  fmt: str = FMT,
517
517
  fields: str | None = FIELDS,
518
518
  ) -> None:
519
- """Get media using an audio track."""
519
+ """[temporarily unavailable] Get media using an audio track."""
520
520
  data = api_get(f"/v1/instagram/audio/{audio_id}/media", {"amount": amount, "cursor": cursor})
521
521
  render(data, fmt, fields)
522
522
 
@@ -527,6 +527,6 @@ def trending_audio(
527
527
  fmt: str = FMT,
528
528
  fields: str | None = FIELDS,
529
529
  ) -> None:
530
- """Get trending audio."""
530
+ """[temporarily unavailable] Get trending audio."""
531
531
  data = api_get("/v1/instagram/audio/trending", {"product": product})
532
532
  render(data, fmt, fields)
@@ -0,0 +1,211 @@
1
+ """Walmart marketplace commands. US-only — no market/domain option."""
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 format: json, csv, table, markdown")
11
+ FIELDS = typer.Option(None, "--fields", "-f", help="Comma-separated fields to include")
12
+
13
+ SORT_HELP = "best_match, best_seller, price_low, price_high, rating_high, new"
14
+
15
+ _ITEM_COLS = "us_item_id,name,price,rating,review_count,seller_name"
16
+
17
+
18
+ def _show(
19
+ data: object, fmt: str, fields: str | None, table_cols: str, key: str | None = None
20
+ ) -> None:
21
+ """Render, defaulting the columns for the human formats only.
22
+
23
+ json and csv keep the whole envelope so a machine-readable export never
24
+ silently drops fields; table/markdown drill to `key` (the response's own
25
+ row list, when `output.render` cannot find it) and default to `table_cols`.
26
+ """
27
+ if fmt in ("table", "markdown"):
28
+ if key and isinstance(data, dict):
29
+ data = data.get(key, data)
30
+ fields = fields or table_cols
31
+ render(data, fmt, fields)
32
+
33
+
34
+ @app.command()
35
+ def search(
36
+ query: str = typer.Argument(..., help="Search keywords, e.g. 'laptop'"),
37
+ page: int = typer.Option(1, "--page", "-p", help="1-10"),
38
+ sort: str | None = typer.Option(None, "--sort", "-s", help=SORT_HELP),
39
+ min_price: float | None = typer.Option(None, "--min-price"),
40
+ max_price: float | None = typer.Option(None, "--max-price"),
41
+ facet: str | None = typer.Option(None, "--facet", help="Facet filter, e.g. 'brand:HP'"),
42
+ fmt: str = FMT,
43
+ fields: str | None = FIELDS,
44
+ ) -> None:
45
+ """Search walmart.com products by keyword."""
46
+ data = api_get(
47
+ "/v1/walmart/search",
48
+ {
49
+ "query": query,
50
+ "page": page,
51
+ "sort": sort,
52
+ "min_price": min_price,
53
+ "max_price": max_price,
54
+ "facet": facet,
55
+ },
56
+ )
57
+ _show(data, fmt, fields, _ITEM_COLS)
58
+
59
+
60
+ @app.command()
61
+ def category(
62
+ path: str = typer.Argument(..., help="Category browse path, e.g. 'electronics/3944'"),
63
+ page: int = typer.Option(1, "--page", "-p", help="1-11"),
64
+ min_price: float | None = typer.Option(None, "--min-price"),
65
+ max_price: float | None = typer.Option(None, "--max-price"),
66
+ facet: str | None = typer.Option(None, "--facet", help="Facet filter, e.g. 'brand:HP'"),
67
+ fmt: str = FMT,
68
+ fields: str | None = FIELDS,
69
+ ) -> None:
70
+ """Browse a walmart.com category.
71
+
72
+ No --sort: Walmart's browse surface ignores it. Use `search` for ordering.
73
+ """
74
+ data = api_get(
75
+ "/v1/walmart/category",
76
+ {
77
+ "path": path,
78
+ "page": page,
79
+ "min_price": min_price,
80
+ "max_price": max_price,
81
+ "facet": facet,
82
+ },
83
+ )
84
+ _show(data, fmt, fields, _ITEM_COLS)
85
+
86
+
87
+ @app.command()
88
+ def deals(
89
+ page: int = typer.Option(1, "--page", "-p", help="1-11"),
90
+ min_price: float | None = typer.Option(None, "--min-price"),
91
+ max_price: float | None = typer.Option(None, "--max-price"),
92
+ fmt: str = FMT,
93
+ fields: str | None = FIELDS,
94
+ ) -> None:
95
+ """List current walmart.com deals (rollbacks and clearance)."""
96
+ data = api_get(
97
+ "/v1/walmart/deals",
98
+ {"page": page, "min_price": min_price, "max_price": max_price},
99
+ )
100
+ _show(data, fmt, fields, _ITEM_COLS)
101
+
102
+
103
+ @app.command()
104
+ def autocomplete(
105
+ query: str = typer.Argument(..., help="Partial search term, e.g. 'lapt'"),
106
+ fmt: str = FMT,
107
+ fields: str | None = FIELDS,
108
+ ) -> None:
109
+ """Get walmart.com search autocomplete suggestions."""
110
+ data = api_get("/v1/walmart/autocomplete", {"query": query})
111
+ _show(data, fmt, fields, "query,type,department_name,url", key="suggestions")
112
+
113
+
114
+ @app.command()
115
+ def product(
116
+ item_id: str = typer.Argument(..., help="Walmart US item ID"),
117
+ fmt: str = FMT,
118
+ fields: str | None = FIELDS,
119
+ ) -> None:
120
+ """Get a full Walmart product detail page (PDP)."""
121
+ data = api_get(f"/v1/walmart/products/{item_id}")
122
+ _show(data, fmt, fields, "us_item_id,name,brand,price,rating,review_count,availability_status")
123
+
124
+
125
+ @app.command()
126
+ def reviews(
127
+ item_id: str = typer.Argument(..., help="Walmart US item ID"),
128
+ page: int = typer.Option(1, "--page", "-p", help="1-100"),
129
+ sort: str | None = typer.Option(
130
+ None,
131
+ "--sort",
132
+ "-s",
133
+ help="relevancy, submission-desc, submission-asc, rating-desc, rating-asc, helpful",
134
+ ),
135
+ fmt: str = FMT,
136
+ fields: str | None = FIELDS,
137
+ ) -> None:
138
+ """Get a Walmart product's reviews and rating distribution."""
139
+ data = api_get(f"/v1/walmart/products/{item_id}/reviews", {"page": page, "sort": sort})
140
+ _show(
141
+ data,
142
+ fmt,
143
+ fields,
144
+ "review_id,rating,title,author,submitted_at,verified_purchase",
145
+ key="reviews",
146
+ )
147
+
148
+
149
+ @app.command()
150
+ def seller(
151
+ seller_id: str = typer.Argument(..., help="Numeric catalog seller ID, e.g. 101040442"),
152
+ fmt: str = FMT,
153
+ fields: str | None = FIELDS,
154
+ ) -> None:
155
+ """Get a Walmart marketplace seller profile."""
156
+ data = api_get(f"/v1/walmart/sellers/{seller_id}")
157
+ _show(
158
+ data,
159
+ fmt,
160
+ fields,
161
+ "seller_id,name,seller_type,rating,review_count,city,state",
162
+ key="seller",
163
+ )
164
+
165
+
166
+ @app.command(name="seller-products")
167
+ def seller_products(
168
+ seller_id: str = typer.Argument(..., help="Numeric catalog seller ID, e.g. 101040442"),
169
+ query: str = typer.Option(..., "--query", "-q", help="Search text (required)"),
170
+ page: int = typer.Option(1, "--page", "-p", help="1-10"),
171
+ sort: str | None = typer.Option(None, "--sort", "-s", help=SORT_HELP),
172
+ fmt: str = FMT,
173
+ fields: str | None = FIELDS,
174
+ ) -> None:
175
+ """Search a Walmart seller's catalogue.
176
+
177
+ --query is required: the storefront renders client-side, so a seller's
178
+ listings are only reachable through search filtered to that seller.
179
+ """
180
+ data = api_get(
181
+ f"/v1/walmart/sellers/{seller_id}/products",
182
+ {"query": query, "page": page, "sort": sort},
183
+ )
184
+ _show(data, fmt, fields, _ITEM_COLS)
185
+
186
+
187
+ @app.command()
188
+ def store(
189
+ store_id: str = typer.Argument(..., help="Walmart store number, e.g. 100"),
190
+ fmt: str = FMT,
191
+ fields: str | None = FIELDS,
192
+ ) -> None:
193
+ """Get a Walmart store: address, hours, services and nearby stores."""
194
+ data = api_get(f"/v1/walmart/stores/{store_id}")
195
+ _show(
196
+ data,
197
+ fmt,
198
+ fields,
199
+ "store_id,name,city,state,postal_code,phone,open_24_hours",
200
+ key="store",
201
+ )
202
+
203
+
204
+ @app.command()
205
+ def markets(
206
+ fmt: str = FMT,
207
+ fields: str | None = FIELDS,
208
+ ) -> None:
209
+ """List supported Walmart markets (US only)."""
210
+ data = api_get("/v1/walmart/markets")
211
+ _show(data, fmt, fields, "code,name,domain,currency,language", key="markets")
@@ -0,0 +1,102 @@
1
+ """Yandex search commands. Default market tr (yandex.com.tr) clears anti-bot
2
+ most reliably. Reverse image search is the standout feature."""
3
+
4
+ import typer
5
+
6
+ from scrapebadger_cli.client import api_get
7
+ from scrapebadger_cli.output import render
8
+
9
+ app = typer.Typer(no_args_is_help=True)
10
+
11
+ FMT = typer.Option("json", "--output", "-o", help="Output format: json, csv, table, markdown")
12
+ FIELDS = typer.Option(None, "--fields", "-f", help="Comma-separated fields to include")
13
+
14
+ DOMAIN = typer.Option("tr", "--domain", "-d", help="Market: tr, com, ru, by, kz, uz")
15
+
16
+
17
+ def _show(
18
+ data: object, fmt: str, fields: str | None, table_cols: str, key: str | None = None
19
+ ) -> None:
20
+ """Render, defaulting the columns for the human formats only.
21
+
22
+ json and csv keep the whole envelope so a machine-readable export never
23
+ silently drops fields; table/markdown drill to `key` (the response's own
24
+ row list, when `output.render` cannot find it) and default to `table_cols`.
25
+ """
26
+ if fmt in ("table", "markdown"):
27
+ if key and isinstance(data, dict):
28
+ data = data.get(key, data)
29
+ fields = fields or table_cols
30
+ render(data, fmt, fields)
31
+
32
+
33
+ @app.command()
34
+ def search(
35
+ query: str = typer.Argument(..., help="Search keywords, e.g. 'kofe'"),
36
+ domain: str = DOMAIN,
37
+ page: int = typer.Option(1, "--page", "-p", help="1-25"),
38
+ lr: int | None = typer.Option(None, "--lr", help="Yandex region id (int)"),
39
+ lang: str | None = typer.Option(None, "--lang", help="ru, en, tr, be, kk, uk"),
40
+ fmt: str = FMT,
41
+ fields: str | None = FIELDS,
42
+ ) -> None:
43
+ """Search Yandex web results by keyword."""
44
+ data = api_get(
45
+ "/v1/yandex/search",
46
+ {"query": query, "domain": domain, "page": page, "lr": lr, "lang": lang},
47
+ )
48
+ _show(
49
+ data,
50
+ fmt,
51
+ fields,
52
+ "position,title,url,displayed_url,domain,snippet",
53
+ key="organic_results",
54
+ )
55
+
56
+
57
+ @app.command()
58
+ def images(
59
+ query: str = typer.Argument(..., help="Search keywords, e.g. 'sunset'"),
60
+ domain: str = DOMAIN,
61
+ page: int = typer.Option(1, "--page", "-p", help="1-25"),
62
+ fmt: str = FMT,
63
+ fields: str | None = FIELDS,
64
+ ) -> None:
65
+ """Search Yandex Images by text query."""
66
+ data = api_get(
67
+ "/v1/yandex/images/search",
68
+ {"query": query, "domain": domain, "page": page},
69
+ )
70
+ _show(
71
+ data,
72
+ fmt,
73
+ fields,
74
+ "position,title,source,source_url,thumbnail",
75
+ key="results",
76
+ )
77
+
78
+
79
+ @app.command()
80
+ def reverse(
81
+ image_url: str = typer.Argument(..., help="Public URL of the image to search by"),
82
+ domain: str = DOMAIN,
83
+ fmt: str = FMT,
84
+ fields: str | None = FIELDS,
85
+ ) -> None:
86
+ """Reverse image search on Yandex (CBIR) — find where an image appears
87
+ online, plus visually similar images, tags and other sizes."""
88
+ data = api_get(
89
+ "/v1/yandex/images/reverse",
90
+ {"image_url": image_url, "domain": domain},
91
+ )
92
+ _show(data, fmt, fields, "title,description,url,domain", key="sites")
93
+
94
+
95
+ @app.command()
96
+ def markets(
97
+ fmt: str = FMT,
98
+ fields: str | None = FIELDS,
99
+ ) -> None:
100
+ """List supported Yandex markets (tr, com, ru, by, kz, uz)."""
101
+ data = api_get("/v1/yandex/markets")
102
+ _show(data, fmt, fields, "code,name,domain,country_code,default_lr,lang", key="markets")
@@ -6,8 +6,11 @@ from scrapebadger_cli.commands import (
6
6
  amazon,
7
7
  apartments,
8
8
  auth,
9
+ baidu,
10
+ bing,
9
11
  chatgpt,
10
12
  depop,
13
+ duckduckgo,
11
14
  ebay,
12
15
  facebook,
13
16
  google,
@@ -24,7 +27,9 @@ from scrapebadger_cli.commands import (
24
27
  tiktok,
25
28
  twitter,
26
29
  vinted,
30
+ walmart,
27
31
  web,
32
+ yandex,
28
33
  youtube,
29
34
  zillow,
30
35
  )
@@ -42,6 +47,7 @@ app.add_typer(stream.app, name="stream", help="Real-time streaming commands")
42
47
  app.add_typer(vinted.app, name="vinted", help="Vinted marketplace commands")
43
48
  app.add_typer(amazon.app, name="amazon", help="Amazon marketplace commands")
44
49
  app.add_typer(ebay.app, name="ebay", help="eBay marketplace commands")
50
+ app.add_typer(walmart.app, name="walmart", help="Walmart marketplace commands")
45
51
  app.add_typer(idealista.app, name="idealista", help="Idealista real-estate commands")
46
52
  app.add_typer(immobiliare.app, name="immobiliare", help="Immobiliare.it real-estate commands")
47
53
  app.add_typer(leboncoin.app, name="leboncoin", help="Leboncoin classifieds commands")
@@ -50,6 +56,10 @@ app.add_typer(youtube.app, name="youtube", help="YouTube commands")
50
56
  app.add_typer(zillow.app, name="zillow", help="Zillow real-estate commands")
51
57
  app.add_typer(web.app, name="web", help="Web scraping commands")
52
58
  app.add_typer(google.app, name="google", help="Google scraper commands")
59
+ app.add_typer(bing.app, name="bing", help="Bing search engine commands")
60
+ app.add_typer(baidu.app, name="baidu", help="Baidu search engine commands")
61
+ app.add_typer(duckduckgo.app, name="duckduckgo", help="DuckDuckGo search commands")
62
+ app.add_typer(yandex.app, name="yandex", help="Yandex search commands")
53
63
  app.add_typer(realtor.app, name="realtor", help="Realtor real-estate commands")
54
64
  app.add_typer(loopnet.app, name="loopnet", help="LoopNet commercial real estate commands")
55
65
  app.add_typer(depop.app, name="depop", help="Depop marketplace commands")
@@ -228,7 +228,7 @@ wheels = [
228
228
 
229
229
  [[package]]
230
230
  name = "scrapebadger-cli"
231
- version = "0.6.0"
231
+ version = "0.8.0"
232
232
  source = { editable = "." }
233
233
  dependencies = [
234
234
  { name = "httpx" },