scrapebadger-cli 0.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- scrapebadger_cli-0.2.1/.gitignore +188 -0
- scrapebadger_cli-0.2.1/PKG-INFO +41 -0
- scrapebadger_cli-0.2.1/README.md +19 -0
- scrapebadger_cli-0.2.1/pyproject.toml +38 -0
- scrapebadger_cli-0.2.1/src/scrapebadger_cli/__init__.py +1 -0
- scrapebadger_cli-0.2.1/src/scrapebadger_cli/client.py +57 -0
- scrapebadger_cli-0.2.1/src/scrapebadger_cli/commands/__init__.py +1 -0
- scrapebadger_cli-0.2.1/src/scrapebadger_cli/commands/amazon.py +227 -0
- scrapebadger_cli-0.2.1/src/scrapebadger_cli/commands/auth.py +52 -0
- scrapebadger_cli-0.2.1/src/scrapebadger_cli/commands/ebay.py +217 -0
- scrapebadger_cli-0.2.1/src/scrapebadger_cli/commands/google.py +1117 -0
- scrapebadger_cli-0.2.1/src/scrapebadger_cli/commands/idealista.py +220 -0
- scrapebadger_cli-0.2.1/src/scrapebadger_cli/commands/immobiliare.py +151 -0
- scrapebadger_cli-0.2.1/src/scrapebadger_cli/commands/leboncoin.py +154 -0
- scrapebadger_cli-0.2.1/src/scrapebadger_cli/commands/loopnet.py +89 -0
- scrapebadger_cli-0.2.1/src/scrapebadger_cli/commands/realtor.py +91 -0
- scrapebadger_cli-0.2.1/src/scrapebadger_cli/commands/reddit.py +173 -0
- scrapebadger_cli-0.2.1/src/scrapebadger_cli/commands/stream.py +270 -0
- scrapebadger_cli-0.2.1/src/scrapebadger_cli/commands/tiktok.py +283 -0
- scrapebadger_cli-0.2.1/src/scrapebadger_cli/commands/twitter.py +235 -0
- scrapebadger_cli-0.2.1/src/scrapebadger_cli/commands/vinted.py +105 -0
- scrapebadger_cli-0.2.1/src/scrapebadger_cli/commands/web.py +117 -0
- scrapebadger_cli-0.2.1/src/scrapebadger_cli/commands/youtube.py +625 -0
- scrapebadger_cli-0.2.1/src/scrapebadger_cli/commands/zillow.py +105 -0
- scrapebadger_cli-0.2.1/src/scrapebadger_cli/config.py +69 -0
- scrapebadger_cli-0.2.1/src/scrapebadger_cli/main.py +52 -0
- scrapebadger_cli-0.2.1/src/scrapebadger_cli/output.py +116 -0
- scrapebadger_cli-0.2.1/uv.lock +410 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# =============================================================================
|
|
2
|
+
# ScrapeBadger - Git Ignore
|
|
3
|
+
# =============================================================================
|
|
4
|
+
|
|
5
|
+
# -----------------------------------------------------------------------------
|
|
6
|
+
# Python
|
|
7
|
+
# -----------------------------------------------------------------------------
|
|
8
|
+
__pycache__/
|
|
9
|
+
*.py[cod]
|
|
10
|
+
*$py.class
|
|
11
|
+
*.so
|
|
12
|
+
.Python
|
|
13
|
+
build/
|
|
14
|
+
develop-eggs/
|
|
15
|
+
dist/
|
|
16
|
+
downloads/
|
|
17
|
+
eggs/
|
|
18
|
+
.eggs/
|
|
19
|
+
# Python build artifacts (root level only, not src/lib/)
|
|
20
|
+
/lib/
|
|
21
|
+
/lib64/
|
|
22
|
+
parts/
|
|
23
|
+
sdist/
|
|
24
|
+
var/
|
|
25
|
+
wheels/
|
|
26
|
+
*.egg-info/
|
|
27
|
+
.installed.cfg
|
|
28
|
+
*.egg
|
|
29
|
+
MANIFEST
|
|
30
|
+
|
|
31
|
+
# Virtual environments
|
|
32
|
+
.venv/
|
|
33
|
+
venv/
|
|
34
|
+
ENV/
|
|
35
|
+
env/
|
|
36
|
+
|
|
37
|
+
# UV
|
|
38
|
+
.uv/
|
|
39
|
+
# Note: uv.lock is committed for reproducible builds
|
|
40
|
+
|
|
41
|
+
# Pytest
|
|
42
|
+
.pytest_cache/
|
|
43
|
+
htmlcov/
|
|
44
|
+
.coverage
|
|
45
|
+
.coverage.*
|
|
46
|
+
coverage.xml
|
|
47
|
+
*.cover
|
|
48
|
+
.hypothesis/
|
|
49
|
+
|
|
50
|
+
# mypy
|
|
51
|
+
.mypy_cache/
|
|
52
|
+
.dmypy.json
|
|
53
|
+
dmypy.json
|
|
54
|
+
|
|
55
|
+
# Ruff
|
|
56
|
+
.ruff_cache/
|
|
57
|
+
|
|
58
|
+
# -----------------------------------------------------------------------------
|
|
59
|
+
# Node.js / Next.js
|
|
60
|
+
# -----------------------------------------------------------------------------
|
|
61
|
+
node_modules/
|
|
62
|
+
.next/
|
|
63
|
+
out/
|
|
64
|
+
.nuxt/
|
|
65
|
+
dist/
|
|
66
|
+
npm-debug.log*
|
|
67
|
+
yarn-debug.log*
|
|
68
|
+
yarn-error.log*
|
|
69
|
+
.pnpm-debug.log*
|
|
70
|
+
|
|
71
|
+
# TypeScript
|
|
72
|
+
*.tsbuildinfo
|
|
73
|
+
|
|
74
|
+
# -----------------------------------------------------------------------------
|
|
75
|
+
# Environment & Secrets
|
|
76
|
+
# -----------------------------------------------------------------------------
|
|
77
|
+
.env
|
|
78
|
+
.env.local
|
|
79
|
+
.env.*.local
|
|
80
|
+
.env.development
|
|
81
|
+
.env.test
|
|
82
|
+
.env.production
|
|
83
|
+
*.pem
|
|
84
|
+
*.key
|
|
85
|
+
secrets/
|
|
86
|
+
.secrets/
|
|
87
|
+
|
|
88
|
+
# -----------------------------------------------------------------------------
|
|
89
|
+
# IDE & Editors
|
|
90
|
+
# -----------------------------------------------------------------------------
|
|
91
|
+
.idea/
|
|
92
|
+
.vscode/
|
|
93
|
+
*.swp
|
|
94
|
+
*.swo
|
|
95
|
+
*~
|
|
96
|
+
.project
|
|
97
|
+
.classpath
|
|
98
|
+
.settings/
|
|
99
|
+
*.sublime-workspace
|
|
100
|
+
*.sublime-project
|
|
101
|
+
|
|
102
|
+
# -----------------------------------------------------------------------------
|
|
103
|
+
# OS
|
|
104
|
+
# -----------------------------------------------------------------------------
|
|
105
|
+
.DS_Store
|
|
106
|
+
.DS_Store?
|
|
107
|
+
._*
|
|
108
|
+
.Spotlight-V100
|
|
109
|
+
.Trashes
|
|
110
|
+
ehthumbs.db
|
|
111
|
+
Thumbs.db
|
|
112
|
+
|
|
113
|
+
# -----------------------------------------------------------------------------
|
|
114
|
+
# Docker
|
|
115
|
+
# -----------------------------------------------------------------------------
|
|
116
|
+
docker-compose.override.yml
|
|
117
|
+
.docker/
|
|
118
|
+
|
|
119
|
+
# -----------------------------------------------------------------------------
|
|
120
|
+
# Terraform
|
|
121
|
+
# -----------------------------------------------------------------------------
|
|
122
|
+
*.tfstate
|
|
123
|
+
*.tfstate.*
|
|
124
|
+
*.tfstate.backup
|
|
125
|
+
.terraform/
|
|
126
|
+
.terraform.lock.hcl
|
|
127
|
+
terraform.tfvars
|
|
128
|
+
*.auto.tfvars
|
|
129
|
+
crash.log
|
|
130
|
+
crash.*.log
|
|
131
|
+
override.tf
|
|
132
|
+
override.tf.json
|
|
133
|
+
*_override.tf
|
|
134
|
+
*_override.tf.json
|
|
135
|
+
github-secrets.env
|
|
136
|
+
|
|
137
|
+
# -----------------------------------------------------------------------------
|
|
138
|
+
# Logs & Temp Files
|
|
139
|
+
# -----------------------------------------------------------------------------
|
|
140
|
+
*.log
|
|
141
|
+
logs/
|
|
142
|
+
!apps/portal/src/**/logs/
|
|
143
|
+
tmp/
|
|
144
|
+
temp/
|
|
145
|
+
*.tmp
|
|
146
|
+
*.bak
|
|
147
|
+
|
|
148
|
+
# -----------------------------------------------------------------------------
|
|
149
|
+
# Build Artifacts
|
|
150
|
+
# -----------------------------------------------------------------------------
|
|
151
|
+
*.pyc
|
|
152
|
+
*.pyo
|
|
153
|
+
*.exe
|
|
154
|
+
*.dll
|
|
155
|
+
*.dylib
|
|
156
|
+
|
|
157
|
+
# -----------------------------------------------------------------------------
|
|
158
|
+
# Project Specific
|
|
159
|
+
# -----------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
# Database backups
|
|
162
|
+
backups/
|
|
163
|
+
|
|
164
|
+
# Local data (root-only; frontend data/ dirs like apps/portal/src/data
|
|
165
|
+
# are first-class source)
|
|
166
|
+
/data/
|
|
167
|
+
*.db
|
|
168
|
+
*.sqlite
|
|
169
|
+
*.sqlite3
|
|
170
|
+
|
|
171
|
+
# Jupyter
|
|
172
|
+
.ipynb_checkpoints/
|
|
173
|
+
*.ipynb
|
|
174
|
+
|
|
175
|
+
# Test outputs
|
|
176
|
+
test-results/
|
|
177
|
+
playwright-report/
|
|
178
|
+
|
|
179
|
+
# Playwright MCP screenshots
|
|
180
|
+
.playwright-mcp/
|
|
181
|
+
|
|
182
|
+
# Claude Code worktrees (per-task scratch checkouts)
|
|
183
|
+
.claude/worktrees/
|
|
184
|
+
|
|
185
|
+
# TikTok reverse-engineering research scratch (large: full APK, decompiled
|
|
186
|
+
# proprietary binaries, native .so libs, frida-server, handoff tarballs).
|
|
187
|
+
# Local-only — not part of the deployable codebase.
|
|
188
|
+
research/
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: scrapebadger-cli
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: ScrapeBadger CLI — Twitter, Vinted, Google, and Web Scraping from the command line
|
|
5
|
+
Author: ScrapeBadger Team
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: ai-tools,cli,mcp,scrapebadger,twitter,vinted,web-scraping
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
12
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Requires-Dist: httpx>=0.26.0
|
|
15
|
+
Requires-Dist: rich>=13.0.0
|
|
16
|
+
Requires-Dist: typer>=0.12.0
|
|
17
|
+
Requires-Dist: websockets>=12.0
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
20
|
+
Requires-Dist: ruff>=0.2.0; extra == 'dev'
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# ScrapeBadger CLI
|
|
24
|
+
|
|
25
|
+
Command-line interface for ScrapeBadger — Twitter, Vinted, and Web Scraping.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install scrapebadger-cli
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Quick Start
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
scrapebadger auth --api-key sb_live_...
|
|
37
|
+
scrapebadger twitter user elonmusk
|
|
38
|
+
scrapebadger twitter search "AI agents" --output table
|
|
39
|
+
scrapebadger vinted search "nike shoes" --market de
|
|
40
|
+
scrapebadger web scrape https://example.com --js
|
|
41
|
+
```
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# ScrapeBadger CLI
|
|
2
|
+
|
|
3
|
+
Command-line interface for ScrapeBadger — Twitter, Vinted, and Web Scraping.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install scrapebadger-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
scrapebadger auth --api-key sb_live_...
|
|
15
|
+
scrapebadger twitter user elonmusk
|
|
16
|
+
scrapebadger twitter search "AI agents" --output table
|
|
17
|
+
scrapebadger vinted search "nike shoes" --market de
|
|
18
|
+
scrapebadger web scrape https://example.com --js
|
|
19
|
+
```
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "scrapebadger-cli"
|
|
3
|
+
version = "0.2.1"
|
|
4
|
+
description = "ScrapeBadger CLI — Twitter, Vinted, Google, and Web Scraping from the command line"
|
|
5
|
+
requires-python = ">=3.10"
|
|
6
|
+
license = {text = "MIT"}
|
|
7
|
+
authors = [{name = "ScrapeBadger Team"}]
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
keywords = ["scrapebadger", "cli", "web-scraping", "twitter", "vinted", "mcp", "ai-tools"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 4 - Beta",
|
|
12
|
+
"Environment :: Console",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
15
|
+
"Topic :: Software Development :: Libraries",
|
|
16
|
+
]
|
|
17
|
+
dependencies = [
|
|
18
|
+
"typer>=0.12.0",
|
|
19
|
+
"httpx>=0.26.0",
|
|
20
|
+
"rich>=13.0.0",
|
|
21
|
+
"websockets>=12.0",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
scrapebadger = "scrapebadger_cli.main:app"
|
|
26
|
+
|
|
27
|
+
[project.optional-dependencies]
|
|
28
|
+
dev = [
|
|
29
|
+
"pytest>=8.0.0",
|
|
30
|
+
"ruff>=0.2.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[build-system]
|
|
34
|
+
requires = ["hatchling"]
|
|
35
|
+
build-backend = "hatchling.build"
|
|
36
|
+
|
|
37
|
+
[tool.hatch.build.targets.wheel]
|
|
38
|
+
packages = ["src/scrapebadger_cli"]
|
|
@@ -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]")
|