scrapebadger-cli 0.2.1__py3-none-any.whl
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/__init__.py +1 -0
- scrapebadger_cli/client.py +57 -0
- scrapebadger_cli/commands/__init__.py +1 -0
- scrapebadger_cli/commands/amazon.py +227 -0
- scrapebadger_cli/commands/auth.py +52 -0
- scrapebadger_cli/commands/ebay.py +217 -0
- scrapebadger_cli/commands/google.py +1117 -0
- scrapebadger_cli/commands/idealista.py +220 -0
- scrapebadger_cli/commands/immobiliare.py +151 -0
- scrapebadger_cli/commands/leboncoin.py +154 -0
- scrapebadger_cli/commands/loopnet.py +89 -0
- scrapebadger_cli/commands/realtor.py +91 -0
- scrapebadger_cli/commands/reddit.py +173 -0
- scrapebadger_cli/commands/stream.py +270 -0
- scrapebadger_cli/commands/tiktok.py +283 -0
- scrapebadger_cli/commands/twitter.py +235 -0
- scrapebadger_cli/commands/vinted.py +105 -0
- scrapebadger_cli/commands/web.py +117 -0
- scrapebadger_cli/commands/youtube.py +625 -0
- scrapebadger_cli/commands/zillow.py +105 -0
- scrapebadger_cli/config.py +69 -0
- scrapebadger_cli/main.py +52 -0
- scrapebadger_cli/output.py +116 -0
- scrapebadger_cli-0.2.1.dist-info/METADATA +41 -0
- scrapebadger_cli-0.2.1.dist-info/RECORD +27 -0
- scrapebadger_cli-0.2.1.dist-info/WHEEL +4 -0
- scrapebadger_cli-0.2.1.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""ScrapeBadger CLI — Twitter, Vinted, and Web Scraping from the command line."""
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""HTTP client for ScrapeBadger API."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from scrapebadger_cli.config import get_api_key
|
|
10
|
+
|
|
11
|
+
BASE_URL = os.environ.get("SCRAPEBADGER_API_URL", "https://scrapebadger.com")
|
|
12
|
+
_client: httpx.Client | None = None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _get_client() -> httpx.Client:
|
|
16
|
+
global _client
|
|
17
|
+
if _client is None:
|
|
18
|
+
api_key = get_api_key()
|
|
19
|
+
if not api_key:
|
|
20
|
+
typer.echo(
|
|
21
|
+
"No API key found. Set SCRAPEBADGER_API_KEY or run: scrapebadger auth",
|
|
22
|
+
err=True,
|
|
23
|
+
)
|
|
24
|
+
raise typer.Exit(1)
|
|
25
|
+
_client = httpx.Client(
|
|
26
|
+
base_url=BASE_URL,
|
|
27
|
+
headers={"X-API-Key": api_key},
|
|
28
|
+
timeout=300.0,
|
|
29
|
+
)
|
|
30
|
+
return _client
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def api_get(path: str, params: dict[str, Any] | None = None) -> dict | list:
|
|
34
|
+
"""Make a GET request to the ScrapeBadger API."""
|
|
35
|
+
clean = {k: v for k, v in (params or {}).items() if v is not None}
|
|
36
|
+
resp = _get_client().get(path, params=clean or None)
|
|
37
|
+
return _handle_response(resp)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def api_post(path: str, json_body: dict[str, Any] | None = None) -> dict | list:
|
|
41
|
+
"""Make a POST request to the ScrapeBadger API."""
|
|
42
|
+
resp = _get_client().post(path, json=json_body)
|
|
43
|
+
return _handle_response(resp)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _handle_response(resp: httpx.Response) -> dict | list:
|
|
47
|
+
"""Handle API response, exit on errors."""
|
|
48
|
+
if resp.status_code >= 400:
|
|
49
|
+
try:
|
|
50
|
+
detail = resp.json().get("detail", resp.text)
|
|
51
|
+
except Exception:
|
|
52
|
+
detail = resp.text
|
|
53
|
+
typer.echo(f"Error ({resp.status_code}): {detail}", err=True)
|
|
54
|
+
raise typer.Exit(1)
|
|
55
|
+
if resp.status_code == 204:
|
|
56
|
+
return {"status": "deleted"}
|
|
57
|
+
return resp.json()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI command modules."""
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""Amazon marketplace 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
|
+
DOMAIN = typer.Option("com", "--domain", "-d", help="Amazon marketplace TLD (com, co.uk, de, etc.)")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@app.command()
|
|
16
|
+
def search(
|
|
17
|
+
query: str = typer.Argument(..., help="Search text"),
|
|
18
|
+
domain: str = DOMAIN,
|
|
19
|
+
page: int = typer.Option(1, "--page", "-p"),
|
|
20
|
+
sort_by: str | None = typer.Option(None, "--sort-by", help="e.g. price_low_to_high"),
|
|
21
|
+
category: str | None = typer.Option(None, "--category", help="Department/search alias"),
|
|
22
|
+
min_price: float | None = typer.Option(None, "--min-price"),
|
|
23
|
+
max_price: float | None = typer.Option(None, "--max-price"),
|
|
24
|
+
zip_code: str | None = typer.Option(None, "--zip", help="Delivery ZIP/postal code"),
|
|
25
|
+
fmt: str = FMT,
|
|
26
|
+
fields: str | None = FIELDS,
|
|
27
|
+
) -> None:
|
|
28
|
+
"""Search the Amazon catalog for products."""
|
|
29
|
+
data = api_get(
|
|
30
|
+
"/v1/amazon/search",
|
|
31
|
+
{
|
|
32
|
+
"query": query,
|
|
33
|
+
"domain": domain,
|
|
34
|
+
"page": page,
|
|
35
|
+
"sort_by": sort_by,
|
|
36
|
+
"category": category,
|
|
37
|
+
"min_price": min_price,
|
|
38
|
+
"max_price": max_price,
|
|
39
|
+
"zip": zip_code,
|
|
40
|
+
},
|
|
41
|
+
)
|
|
42
|
+
render(data, fmt, fields)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@app.command()
|
|
46
|
+
def autocomplete(
|
|
47
|
+
query: str = typer.Argument(..., help="Partial search text"),
|
|
48
|
+
domain: str = DOMAIN,
|
|
49
|
+
fmt: str = FMT,
|
|
50
|
+
fields: str | None = FIELDS,
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Get Amazon keyword autocomplete suggestions."""
|
|
53
|
+
data = api_get("/v1/amazon/autocomplete", {"query": query, "domain": domain})
|
|
54
|
+
render(data, fmt, fields)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@app.command()
|
|
58
|
+
def product(
|
|
59
|
+
asin: str = typer.Argument(..., help="Amazon ASIN"),
|
|
60
|
+
domain: str = DOMAIN,
|
|
61
|
+
zip_code: str | None = typer.Option(None, "--zip", help="Delivery ZIP/postal code"),
|
|
62
|
+
language: str | None = typer.Option(None, "--language", help="e.g. en_US"),
|
|
63
|
+
fmt: str = FMT,
|
|
64
|
+
fields: str | None = FIELDS,
|
|
65
|
+
) -> None:
|
|
66
|
+
"""Get a full product detail page (PDP) by ASIN."""
|
|
67
|
+
data = api_get(
|
|
68
|
+
f"/v1/amazon/products/{asin}",
|
|
69
|
+
{"domain": domain, "zip": zip_code, "language": language},
|
|
70
|
+
)
|
|
71
|
+
render(data, fmt, fields)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@app.command()
|
|
75
|
+
def offers(
|
|
76
|
+
asin: str = typer.Argument(..., help="Amazon ASIN"),
|
|
77
|
+
domain: str = DOMAIN,
|
|
78
|
+
zip_code: str | None = typer.Option(None, "--zip", help="Delivery ZIP/postal code"),
|
|
79
|
+
fmt: str = FMT,
|
|
80
|
+
fields: str | None = FIELDS,
|
|
81
|
+
) -> None:
|
|
82
|
+
"""Get all seller offers (buybox + competing) for a product."""
|
|
83
|
+
data = api_get(f"/v1/amazon/products/{asin}/offers", {"domain": domain, "zip": zip_code})
|
|
84
|
+
render(data, fmt, fields)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@app.command()
|
|
88
|
+
def reviews(
|
|
89
|
+
asin: str = typer.Argument(..., help="Amazon ASIN"),
|
|
90
|
+
domain: str = DOMAIN,
|
|
91
|
+
page: int = typer.Option(1, "--page", "-p"),
|
|
92
|
+
sort_by: str | None = typer.Option(None, "--sort-by", help="helpful or recent"),
|
|
93
|
+
star: str | None = typer.Option(None, "--star", help="e.g. five_star, critical"),
|
|
94
|
+
verified_only: bool = typer.Option(False, "--verified-only"),
|
|
95
|
+
media_only: bool = typer.Option(False, "--media-only"),
|
|
96
|
+
fmt: str = FMT,
|
|
97
|
+
fields: str | None = FIELDS,
|
|
98
|
+
) -> None:
|
|
99
|
+
"""Get customer reviews for a product."""
|
|
100
|
+
data = api_get(
|
|
101
|
+
f"/v1/amazon/products/{asin}/reviews",
|
|
102
|
+
{
|
|
103
|
+
"domain": domain,
|
|
104
|
+
"page": page,
|
|
105
|
+
"sort_by": sort_by,
|
|
106
|
+
"star": star,
|
|
107
|
+
"verified_only": verified_only,
|
|
108
|
+
"media_only": media_only,
|
|
109
|
+
},
|
|
110
|
+
)
|
|
111
|
+
render(data, fmt, fields)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@app.command()
|
|
115
|
+
def bestsellers(
|
|
116
|
+
domain: str = DOMAIN,
|
|
117
|
+
category: str | None = typer.Option(None, "--category", help="Browse node ID"),
|
|
118
|
+
page: int = typer.Option(1, "--page", "-p"),
|
|
119
|
+
fmt: str = FMT,
|
|
120
|
+
fields: str | None = FIELDS,
|
|
121
|
+
) -> None:
|
|
122
|
+
"""Get an Amazon Best Sellers list."""
|
|
123
|
+
data = api_get("/v1/amazon/bestsellers", {"domain": domain, "category": category, "page": page})
|
|
124
|
+
render(data, fmt, fields)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@app.command(name="new-releases")
|
|
128
|
+
def new_releases(
|
|
129
|
+
domain: str = DOMAIN,
|
|
130
|
+
category: str | None = typer.Option(None, "--category", help="Browse node ID"),
|
|
131
|
+
page: int = typer.Option(1, "--page", "-p"),
|
|
132
|
+
fmt: str = FMT,
|
|
133
|
+
fields: str | None = FIELDS,
|
|
134
|
+
) -> None:
|
|
135
|
+
"""Get an Amazon New Releases list."""
|
|
136
|
+
data = api_get(
|
|
137
|
+
"/v1/amazon/new-releases", {"domain": domain, "category": category, "page": page}
|
|
138
|
+
)
|
|
139
|
+
render(data, fmt, fields)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@app.command()
|
|
143
|
+
def deals(
|
|
144
|
+
domain: str = DOMAIN,
|
|
145
|
+
category: str | None = typer.Option(None, "--category", help="Department/category"),
|
|
146
|
+
page: int = typer.Option(1, "--page", "-p"),
|
|
147
|
+
fmt: str = FMT,
|
|
148
|
+
fields: str | None = FIELDS,
|
|
149
|
+
) -> None:
|
|
150
|
+
"""Get current Amazon deals (Today's Deals / Lightning Deals)."""
|
|
151
|
+
data = api_get("/v1/amazon/deals", {"domain": domain, "category": category, "page": page})
|
|
152
|
+
render(data, fmt, fields)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@app.command()
|
|
156
|
+
def category(
|
|
157
|
+
node: str = typer.Argument(..., help="Amazon browse node ID"),
|
|
158
|
+
domain: str = DOMAIN,
|
|
159
|
+
page: int = typer.Option(1, "--page", "-p"),
|
|
160
|
+
sort_by: str | None = typer.Option(None, "--sort-by"),
|
|
161
|
+
fmt: str = FMT,
|
|
162
|
+
fields: str | None = FIELDS,
|
|
163
|
+
) -> None:
|
|
164
|
+
"""Browse an Amazon category/department by browse-node ID."""
|
|
165
|
+
data = api_get(
|
|
166
|
+
"/v1/amazon/category",
|
|
167
|
+
{"node": node, "domain": domain, "page": page, "sort_by": sort_by},
|
|
168
|
+
)
|
|
169
|
+
render(data, fmt, fields)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@app.command()
|
|
173
|
+
def seller(
|
|
174
|
+
seller_id: str = typer.Argument(..., help="Amazon seller ID"),
|
|
175
|
+
domain: str = DOMAIN,
|
|
176
|
+
fmt: str = FMT,
|
|
177
|
+
fields: str | None = FIELDS,
|
|
178
|
+
) -> None:
|
|
179
|
+
"""Get an Amazon seller profile (ratings + feedback summary)."""
|
|
180
|
+
data = api_get(f"/v1/amazon/sellers/{seller_id}", {"domain": domain})
|
|
181
|
+
render(data, fmt, fields)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@app.command(name="seller-products")
|
|
185
|
+
def seller_products(
|
|
186
|
+
seller_id: str = typer.Argument(..., help="Amazon seller ID"),
|
|
187
|
+
domain: str = DOMAIN,
|
|
188
|
+
page: int = typer.Option(1, "--page", "-p"),
|
|
189
|
+
fmt: str = FMT,
|
|
190
|
+
fields: str | None = FIELDS,
|
|
191
|
+
) -> None:
|
|
192
|
+
"""Get the storefront product listings of a seller."""
|
|
193
|
+
data = api_get(f"/v1/amazon/sellers/{seller_id}/products", {"domain": domain, "page": page})
|
|
194
|
+
render(data, fmt, fields)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@app.command(name="seller-feedback")
|
|
198
|
+
def seller_feedback(
|
|
199
|
+
seller_id: str = typer.Argument(..., help="Amazon seller ID"),
|
|
200
|
+
domain: str = DOMAIN,
|
|
201
|
+
page: int = typer.Option(1, "--page", "-p"),
|
|
202
|
+
fmt: str = FMT,
|
|
203
|
+
fields: str | None = FIELDS,
|
|
204
|
+
) -> None:
|
|
205
|
+
"""Get buyer feedback entries for a seller."""
|
|
206
|
+
data = api_get(f"/v1/amazon/sellers/{seller_id}/feedback", {"domain": domain, "page": page})
|
|
207
|
+
render(data, fmt, fields)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@app.command()
|
|
211
|
+
def markets(
|
|
212
|
+
fmt: str = FMT,
|
|
213
|
+
fields: str | None = FIELDS,
|
|
214
|
+
) -> None:
|
|
215
|
+
"""List all supported Amazon marketplaces."""
|
|
216
|
+
data = api_get("/v1/amazon/markets")
|
|
217
|
+
render(data, fmt, fields)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@app.command()
|
|
221
|
+
def categories(
|
|
222
|
+
fmt: str = FMT,
|
|
223
|
+
fields: str | None = FIELDS,
|
|
224
|
+
) -> None:
|
|
225
|
+
"""List reference Amazon departments / category aliases."""
|
|
226
|
+
data = api_get("/v1/amazon/categories")
|
|
227
|
+
render(data, fmt, fields)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Auth and account commands."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from scrapebadger_cli.client import api_get
|
|
6
|
+
from scrapebadger_cli.config import clear_api_key, get_api_key, save_api_key
|
|
7
|
+
from scrapebadger_cli.output import console
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def auth(
|
|
11
|
+
api_key: str = typer.Option("", "--api-key", "-k", help="API key (or enter interactively)"),
|
|
12
|
+
show: bool = typer.Option(False, "--show", help="Show current saved API key"),
|
|
13
|
+
) -> None:
|
|
14
|
+
"""Set up or view your ScrapeBadger API key."""
|
|
15
|
+
if show:
|
|
16
|
+
key = get_api_key()
|
|
17
|
+
if key:
|
|
18
|
+
console.print(f"[green]Current API key:[/green] {key[:12]}...{key[-4:]}")
|
|
19
|
+
else:
|
|
20
|
+
console.print("[yellow]No API key configured.[/yellow]")
|
|
21
|
+
return
|
|
22
|
+
|
|
23
|
+
if not api_key:
|
|
24
|
+
api_key = typer.prompt("Enter your ScrapeBadger API key")
|
|
25
|
+
|
|
26
|
+
api_key = api_key.strip()
|
|
27
|
+
if not api_key:
|
|
28
|
+
console.print("[red]API key cannot be empty.[/red]")
|
|
29
|
+
raise typer.Exit(1)
|
|
30
|
+
|
|
31
|
+
save_api_key(api_key)
|
|
32
|
+
console.print(f"[green]API key saved.[/green] ({api_key[:12]}...{api_key[-4:]})")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def credits() -> None:
|
|
36
|
+
"""Check your credit balance."""
|
|
37
|
+
data = api_get("/v1/account/me")
|
|
38
|
+
if isinstance(data, dict):
|
|
39
|
+
balance = data.get("credits_balance", "unknown")
|
|
40
|
+
tier = data.get("tier", "unknown")
|
|
41
|
+
rate_limit = data.get("rate_limit_per_minute", "unknown")
|
|
42
|
+
console.print(f"[green]Credits balance:[/green] {balance}")
|
|
43
|
+
console.print(f"[dim]Tier:[/dim] {tier}")
|
|
44
|
+
console.print(f"[dim]Rate limit:[/dim] {rate_limit}/min")
|
|
45
|
+
else:
|
|
46
|
+
console.print(data)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def logout() -> None:
|
|
50
|
+
"""Remove saved API key."""
|
|
51
|
+
clear_api_key()
|
|
52
|
+
console.print("[green]API key removed.[/green]")
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""eBay marketplace 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
|
+
DOMAIN = typer.Option("com", "--domain", "-d", help="eBay marketplace TLD (com, co.uk, de, etc.)")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@app.command()
|
|
16
|
+
def search(
|
|
17
|
+
query: str = typer.Argument(..., help="Search text"),
|
|
18
|
+
domain: str = DOMAIN,
|
|
19
|
+
category_id: str | None = typer.Option(None, "--category-id", help="Restrict to a category"),
|
|
20
|
+
page: int = typer.Option(1, "--page", "-p"),
|
|
21
|
+
per_page: int | None = typer.Option(None, "--per-page", help="60, 120 or 240"),
|
|
22
|
+
sort_by: str | None = typer.Option(None, "--sort-by", help="e.g. price_low_to_high"),
|
|
23
|
+
condition: str | None = typer.Option(None, "--condition", help="e.g. new, used"),
|
|
24
|
+
buying_format: str | None = typer.Option(
|
|
25
|
+
None, "--buying-format", help="auction, buy_it_now, best_offer"
|
|
26
|
+
),
|
|
27
|
+
min_price: float | None = typer.Option(None, "--min-price"),
|
|
28
|
+
max_price: float | None = typer.Option(None, "--max-price"),
|
|
29
|
+
free_shipping: bool = typer.Option(False, "--free-shipping"),
|
|
30
|
+
fmt: str = FMT,
|
|
31
|
+
fields: str | None = FIELDS,
|
|
32
|
+
) -> None:
|
|
33
|
+
"""Search eBay active listings.
|
|
34
|
+
|
|
35
|
+
Auction cards include bids, current_bid and time_left. Pass
|
|
36
|
+
--buying-format auction to return only auctions.
|
|
37
|
+
"""
|
|
38
|
+
data = api_get(
|
|
39
|
+
"/v1/ebay/search",
|
|
40
|
+
{
|
|
41
|
+
"query": query,
|
|
42
|
+
"domain": domain,
|
|
43
|
+
"category_id": category_id,
|
|
44
|
+
"page": page,
|
|
45
|
+
"per_page": per_page,
|
|
46
|
+
"sort_by": sort_by,
|
|
47
|
+
"condition": condition,
|
|
48
|
+
"buying_format": buying_format,
|
|
49
|
+
"min_price": min_price,
|
|
50
|
+
"max_price": max_price,
|
|
51
|
+
"free_shipping": free_shipping,
|
|
52
|
+
},
|
|
53
|
+
)
|
|
54
|
+
render(data, fmt, fields)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@app.command()
|
|
58
|
+
def completed(
|
|
59
|
+
query: str = typer.Argument(..., help="Search text"),
|
|
60
|
+
domain: str = DOMAIN,
|
|
61
|
+
category_id: str | None = typer.Option(None, "--category-id", help="Restrict to a category"),
|
|
62
|
+
page: int = typer.Option(1, "--page", "-p"),
|
|
63
|
+
per_page: int | None = typer.Option(None, "--per-page", help="60, 120 or 240"),
|
|
64
|
+
sort_by: str | None = typer.Option(None, "--sort-by", help="e.g. ending_soonest"),
|
|
65
|
+
condition: str | None = typer.Option(None, "--condition", help="e.g. new, used"),
|
|
66
|
+
min_price: float | None = typer.Option(None, "--min-price"),
|
|
67
|
+
max_price: float | None = typer.Option(None, "--max-price"),
|
|
68
|
+
fmt: str = FMT,
|
|
69
|
+
fields: str | None = FIELDS,
|
|
70
|
+
) -> None:
|
|
71
|
+
"""Search completed/sold eBay listings (price research)."""
|
|
72
|
+
data = api_get(
|
|
73
|
+
"/v1/ebay/completed",
|
|
74
|
+
{
|
|
75
|
+
"query": query,
|
|
76
|
+
"domain": domain,
|
|
77
|
+
"category_id": category_id,
|
|
78
|
+
"page": page,
|
|
79
|
+
"per_page": per_page,
|
|
80
|
+
"sort_by": sort_by,
|
|
81
|
+
"condition": condition,
|
|
82
|
+
"min_price": min_price,
|
|
83
|
+
"max_price": max_price,
|
|
84
|
+
},
|
|
85
|
+
)
|
|
86
|
+
render(data, fmt, fields)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@app.command()
|
|
90
|
+
def item(
|
|
91
|
+
item_id: str = typer.Argument(..., help="eBay item ID"),
|
|
92
|
+
domain: str = DOMAIN,
|
|
93
|
+
fmt: str = FMT,
|
|
94
|
+
fields: str | None = FIELDS,
|
|
95
|
+
) -> None:
|
|
96
|
+
"""Get a full eBay item detail page by item ID.
|
|
97
|
+
|
|
98
|
+
For auctions this includes bids, current_bid, time_left, end_time_at and
|
|
99
|
+
(for fixed-price / auction-with-Buy-It-Now listings) buy_it_now_price.
|
|
100
|
+
"""
|
|
101
|
+
data = api_get(f"/v1/ebay/items/{item_id}", {"domain": domain})
|
|
102
|
+
render(data, fmt, fields)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@app.command(name="item-reviews")
|
|
106
|
+
def item_reviews(
|
|
107
|
+
item_id: str = typer.Argument(..., help="eBay item ID"),
|
|
108
|
+
domain: str = DOMAIN,
|
|
109
|
+
page: int = typer.Option(1, "--page", "-p"),
|
|
110
|
+
fmt: str = FMT,
|
|
111
|
+
fields: str | None = FIELDS,
|
|
112
|
+
) -> None:
|
|
113
|
+
"""Get product reviews (rating + histogram) for an eBay item."""
|
|
114
|
+
data = api_get(f"/v1/ebay/items/{item_id}/reviews", {"domain": domain, "page": page})
|
|
115
|
+
render(data, fmt, fields)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@app.command()
|
|
119
|
+
def seller(
|
|
120
|
+
username: str = typer.Argument(..., help="eBay seller username"),
|
|
121
|
+
domain: str = DOMAIN,
|
|
122
|
+
fmt: str = FMT,
|
|
123
|
+
fields: str | None = FIELDS,
|
|
124
|
+
) -> None:
|
|
125
|
+
"""Get an eBay seller profile (feedback + store info)."""
|
|
126
|
+
data = api_get(f"/v1/ebay/sellers/{username}", {"domain": domain})
|
|
127
|
+
render(data, fmt, fields)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@app.command(name="seller-items")
|
|
131
|
+
def seller_items(
|
|
132
|
+
username: str = typer.Argument(..., help="eBay seller username"),
|
|
133
|
+
domain: str = DOMAIN,
|
|
134
|
+
query: str | None = typer.Option(None, "--query", "-q", help="Filter the seller's listings"),
|
|
135
|
+
page: int = typer.Option(1, "--page", "-p"),
|
|
136
|
+
per_page: int | None = typer.Option(None, "--per-page", help="60, 120 or 240"),
|
|
137
|
+
fmt: str = FMT,
|
|
138
|
+
fields: str | None = FIELDS,
|
|
139
|
+
) -> None:
|
|
140
|
+
"""Get the active listings of an eBay seller."""
|
|
141
|
+
data = api_get(
|
|
142
|
+
f"/v1/ebay/sellers/{username}/items",
|
|
143
|
+
{"domain": domain, "query": query, "page": page, "per_page": per_page},
|
|
144
|
+
)
|
|
145
|
+
render(data, fmt, fields)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@app.command(name="seller-feedback")
|
|
149
|
+
def seller_feedback(
|
|
150
|
+
username: str = typer.Argument(..., help="eBay seller username"),
|
|
151
|
+
domain: str = DOMAIN,
|
|
152
|
+
page: int = typer.Option(1, "--page", "-p"),
|
|
153
|
+
fmt: str = FMT,
|
|
154
|
+
fields: str | None = FIELDS,
|
|
155
|
+
) -> None:
|
|
156
|
+
"""Get buyer feedback entries for an eBay seller."""
|
|
157
|
+
data = api_get(f"/v1/ebay/sellers/{username}/feedback", {"domain": domain, "page": page})
|
|
158
|
+
render(data, fmt, fields)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@app.command()
|
|
162
|
+
def category(
|
|
163
|
+
category_id: str = typer.Argument(..., help="eBay category ID"),
|
|
164
|
+
domain: str = DOMAIN,
|
|
165
|
+
page: int = typer.Option(1, "--page", "-p"),
|
|
166
|
+
per_page: int | None = typer.Option(None, "--per-page", help="60, 120 or 240"),
|
|
167
|
+
sort_by: str | None = typer.Option(None, "--sort-by"),
|
|
168
|
+
min_price: float | None = typer.Option(None, "--min-price"),
|
|
169
|
+
max_price: float | None = typer.Option(None, "--max-price"),
|
|
170
|
+
fmt: str = FMT,
|
|
171
|
+
fields: str | None = FIELDS,
|
|
172
|
+
) -> None:
|
|
173
|
+
"""Browse eBay listings within a category by category ID."""
|
|
174
|
+
data = api_get(
|
|
175
|
+
f"/v1/ebay/categories/{category_id}/items",
|
|
176
|
+
{
|
|
177
|
+
"domain": domain,
|
|
178
|
+
"page": page,
|
|
179
|
+
"per_page": per_page,
|
|
180
|
+
"sort_by": sort_by,
|
|
181
|
+
"min_price": min_price,
|
|
182
|
+
"max_price": max_price,
|
|
183
|
+
},
|
|
184
|
+
)
|
|
185
|
+
render(data, fmt, fields)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@app.command()
|
|
189
|
+
def categories(
|
|
190
|
+
fmt: str = FMT,
|
|
191
|
+
fields: str | None = FIELDS,
|
|
192
|
+
) -> None:
|
|
193
|
+
"""List reference eBay categories."""
|
|
194
|
+
data = api_get("/v1/ebay/categories")
|
|
195
|
+
render(data, fmt, fields)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@app.command()
|
|
199
|
+
def autocomplete(
|
|
200
|
+
query: str = typer.Argument(..., help="Partial search text"),
|
|
201
|
+
domain: str = DOMAIN,
|
|
202
|
+
fmt: str = FMT,
|
|
203
|
+
fields: str | None = FIELDS,
|
|
204
|
+
) -> None:
|
|
205
|
+
"""Get eBay keyword autocomplete suggestions."""
|
|
206
|
+
data = api_get("/v1/ebay/autocomplete", {"query": query, "domain": domain})
|
|
207
|
+
render(data, fmt, fields)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@app.command()
|
|
211
|
+
def markets(
|
|
212
|
+
fmt: str = FMT,
|
|
213
|
+
fields: str | None = FIELDS,
|
|
214
|
+
) -> None:
|
|
215
|
+
"""List all supported eBay marketplaces."""
|
|
216
|
+
data = api_get("/v1/ebay/markets")
|
|
217
|
+
render(data, fmt, fields)
|