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.
@@ -0,0 +1,105 @@
1
+ """Zillow real-estate 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
+
14
+ @app.command()
15
+ def search(
16
+ location: str = typer.Argument(..., help="City/state, ZIP, address or neighborhood"),
17
+ status: str = typer.Option("for_sale", "--status", "-s", help="for_sale, for_rent, sold"),
18
+ page: int = typer.Option(1, "--page", "-p", help="1-20 (Zillow caps at ~820 results)"),
19
+ sort: str | None = typer.Option(None, "--sort", help="newest, price_low_to_high, etc."),
20
+ price_min: int | None = typer.Option(None, "--price-min"),
21
+ price_max: int | None = typer.Option(None, "--price-max"),
22
+ beds_min: int | None = typer.Option(None, "--beds-min"),
23
+ baths_min: float | None = typer.Option(None, "--baths-min"),
24
+ home_type: str | None = typer.Option(
25
+ None,
26
+ "--home-type",
27
+ help="houses, condos, townhomes, apartments, manufactured, lots, multi_family",
28
+ ),
29
+ sqft_min: int | None = typer.Option(None, "--sqft-min"),
30
+ sqft_max: int | None = typer.Option(None, "--sqft-max"),
31
+ keywords: str | None = typer.Option(None, "--keywords"),
32
+ fmt: str = FMT,
33
+ fields: str | None = FIELDS,
34
+ ) -> None:
35
+ """Search Zillow for for-sale / for-rent / recently-sold properties."""
36
+ data = api_get(
37
+ "/v1/zillow/search",
38
+ {
39
+ "location": location,
40
+ "status": status,
41
+ "page": page,
42
+ "sort": sort,
43
+ "price_min": price_min,
44
+ "price_max": price_max,
45
+ "beds_min": beds_min,
46
+ "baths_min": baths_min,
47
+ "home_type": home_type,
48
+ "sqft_min": sqft_min,
49
+ "sqft_max": sqft_max,
50
+ "keywords": keywords,
51
+ },
52
+ )
53
+ render(data, fmt, fields)
54
+
55
+
56
+ @app.command()
57
+ def property(
58
+ zpid: str = typer.Argument(..., help="Zillow property id (zpid)"),
59
+ fmt: str = FMT,
60
+ fields: str | None = FIELDS,
61
+ ) -> None:
62
+ """Get a Zillow property's full detail by zpid."""
63
+ data = api_get(f"/v1/zillow/property/{zpid}", {})
64
+ render(data, fmt, fields)
65
+
66
+
67
+ @app.command("property-url")
68
+ def property_url(
69
+ url: str = typer.Argument(..., help="Full Zillow /homedetails/... URL"),
70
+ fmt: str = FMT,
71
+ fields: str | None = FIELDS,
72
+ ) -> None:
73
+ """Get a Zillow property's full detail by homedetails URL."""
74
+ data = api_get("/v1/zillow/property", {"url": url})
75
+ render(data, fmt, fields)
76
+
77
+
78
+ @app.command()
79
+ def agent(
80
+ username: str | None = typer.Argument(None, help="Zillow profile username"),
81
+ url: str | None = typer.Option(None, "--url", help="Full Zillow /profile/... URL"),
82
+ fmt: str = FMT,
83
+ fields: str | None = FIELDS,
84
+ ) -> None:
85
+ """Get a Zillow professional's profile and their active listings."""
86
+ data = api_get("/v1/zillow/agent", {"username": username, "url": url})
87
+ render(data, fmt, fields)
88
+
89
+
90
+ @app.command()
91
+ def autocomplete(
92
+ query: str = typer.Argument(..., help="Partial location — city, ZIP, address, neighborhood"),
93
+ fmt: str = FMT,
94
+ fields: str | None = FIELDS,
95
+ ) -> None:
96
+ """Resolve a search term to Zillow regions/addresses (regionId, lat/lng)."""
97
+ data = api_get("/v1/zillow/autocomplete", {"query": query})
98
+ render(data, fmt, fields)
99
+
100
+
101
+ @app.command("markets")
102
+ def list_markets(fmt: str = FMT, fields: str | None = FIELDS) -> None:
103
+ """List Zillow coverage regions (US + Canada)."""
104
+ data = api_get("/v1/zillow/markets", {})
105
+ render(data, fmt, fields)
@@ -0,0 +1,69 @@
1
+ """Configuration and API key management."""
2
+
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+
7
+ CONFIG_DIR = Path.home() / ".config" / "scrapebadger"
8
+ CONFIG_FILE = CONFIG_DIR / "config.json"
9
+ ENV_KEY = "SCRAPEBADGER_API_KEY"
10
+
11
+
12
+ def get_api_key() -> str:
13
+ """Resolve API key from env var, .env file, or config file.
14
+
15
+ Resolution order:
16
+ 1. SCRAPEBADGER_API_KEY environment variable
17
+ 2. .env file in current directory
18
+ 3. ~/.config/scrapebadger/config.json
19
+ """
20
+ # 1. Environment variable
21
+ key = os.environ.get(ENV_KEY, "")
22
+ if key:
23
+ return key
24
+
25
+ # 2. .env file in cwd
26
+ env_path = Path.cwd() / ".env"
27
+ if env_path.exists():
28
+ for line in env_path.read_text().splitlines():
29
+ line = line.strip()
30
+ if line.startswith(f"{ENV_KEY}="):
31
+ val = line.split("=", 1)[1].strip().strip("'\"")
32
+ if val:
33
+ return val
34
+
35
+ # 3. Config file
36
+ if CONFIG_FILE.exists():
37
+ try:
38
+ data = json.loads(CONFIG_FILE.read_text())
39
+ val = data.get("api_key", "")
40
+ if val:
41
+ return val
42
+ except (json.JSONDecodeError, KeyError):
43
+ pass
44
+
45
+ return ""
46
+
47
+
48
+ def save_api_key(key: str) -> None:
49
+ """Save API key to ~/.config/scrapebadger/config.json."""
50
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
51
+ data: dict = {}
52
+ if CONFIG_FILE.exists():
53
+ try:
54
+ data = json.loads(CONFIG_FILE.read_text())
55
+ except json.JSONDecodeError:
56
+ pass
57
+ data["api_key"] = key
58
+ CONFIG_FILE.write_text(json.dumps(data, indent=2) + "\n")
59
+
60
+
61
+ def clear_api_key() -> None:
62
+ """Remove API key from config file."""
63
+ if CONFIG_FILE.exists():
64
+ try:
65
+ data = json.loads(CONFIG_FILE.read_text())
66
+ data.pop("api_key", None)
67
+ CONFIG_FILE.write_text(json.dumps(data, indent=2) + "\n")
68
+ except json.JSONDecodeError:
69
+ pass
@@ -0,0 +1,52 @@
1
+ """ScrapeBadger CLI entry point."""
2
+
3
+ import typer
4
+
5
+ from scrapebadger_cli.commands import (
6
+ amazon,
7
+ auth,
8
+ ebay,
9
+ google,
10
+ idealista,
11
+ immobiliare,
12
+ leboncoin,
13
+ loopnet,
14
+ realtor,
15
+ reddit,
16
+ stream,
17
+ tiktok,
18
+ twitter,
19
+ vinted,
20
+ web,
21
+ youtube,
22
+ zillow,
23
+ )
24
+
25
+ app = typer.Typer(
26
+ name="scrapebadger",
27
+ help="ScrapeBadger CLI — Twitter, Reddit, Vinted, Amazon, eBay, TikTok, YouTube, Google, and Web Scraping from the command line.",
28
+ no_args_is_help=True,
29
+ )
30
+
31
+ app.add_typer(twitter.app, name="twitter", help="Twitter/X data commands")
32
+ app.add_typer(reddit.app, name="reddit", help="Reddit data commands")
33
+ app.add_typer(stream.app, name="stream", help="Real-time streaming commands")
34
+ app.add_typer(vinted.app, name="vinted", help="Vinted marketplace commands")
35
+ app.add_typer(amazon.app, name="amazon", help="Amazon marketplace commands")
36
+ app.add_typer(ebay.app, name="ebay", help="eBay marketplace commands")
37
+ app.add_typer(idealista.app, name="idealista", help="Idealista real-estate commands")
38
+ app.add_typer(immobiliare.app, name="immobiliare", help="Immobiliare.it real-estate commands")
39
+ app.add_typer(leboncoin.app, name="leboncoin", help="Leboncoin classifieds commands")
40
+ app.add_typer(tiktok.app, name="tiktok", help="TikTok data commands")
41
+ app.add_typer(youtube.app, name="youtube", help="YouTube commands")
42
+ app.add_typer(zillow.app, name="zillow", help="Zillow real-estate commands")
43
+ app.add_typer(web.app, name="web", help="Web scraping commands")
44
+ app.add_typer(google.app, name="google", help="Google scraper commands")
45
+ app.add_typer(realtor.app, name="realtor", help="Realtor real-estate commands")
46
+ app.add_typer(loopnet.app, name="loopnet", help="LoopNet commercial real estate commands")
47
+ app.command()(auth.auth)
48
+ app.command(name="credits")(auth.credits)
49
+ app.command(name="logout")(auth.logout)
50
+
51
+ if __name__ == "__main__":
52
+ app()
@@ -0,0 +1,116 @@
1
+ """Output formatting — JSON, CSV, table, or markdown."""
2
+
3
+ import csv
4
+ import json
5
+ import sys
6
+ from typing import Any
7
+
8
+ from rich.console import Console
9
+ from rich.table import Table
10
+
11
+ console = Console()
12
+
13
+ OutputFormat = str # "json" | "csv" | "table" | "markdown"
14
+
15
+
16
+ def render(data: Any, fmt: str = "json", fields: str | None = None) -> None:
17
+ """Render API response in the chosen format.
18
+
19
+ Args:
20
+ data: The API response (dict or list).
21
+ fmt: Output format — json, csv, table, markdown.
22
+ fields: Comma-separated field names to include (optional).
23
+ """
24
+ rows = _normalize_rows(data)
25
+ selected_fields = fields.split(",") if fields else None
26
+
27
+ if selected_fields and rows:
28
+ rows = [{k: r.get(k) for k in selected_fields} for r in rows]
29
+
30
+ if fmt == "json":
31
+ _render_json(rows if len(rows) != 1 else rows[0], data)
32
+ elif fmt == "csv":
33
+ _render_csv(rows)
34
+ elif fmt == "table":
35
+ _render_table(rows)
36
+ elif fmt == "markdown":
37
+ _render_markdown(rows)
38
+ else:
39
+ _render_json(data, data)
40
+
41
+
42
+ def _normalize_rows(data: Any) -> list[dict]:
43
+ """Extract a list of flat dicts from the API response."""
44
+ if isinstance(data, list):
45
+ return [r if isinstance(r, dict) else {"value": r} for r in data]
46
+ if isinstance(data, dict):
47
+ # Look for common list keys in the response
48
+ for key in (
49
+ "data",
50
+ "results",
51
+ "items",
52
+ "tweets",
53
+ "users",
54
+ "rules",
55
+ "logs",
56
+ "tiers",
57
+ "monitors",
58
+ ):
59
+ if key in data and isinstance(data[key], list):
60
+ return [r if isinstance(r, dict) else {"value": r} for r in data[key]]
61
+ return [data]
62
+ return [{"value": data}]
63
+
64
+
65
+ def _render_json(rows: Any, original: Any) -> None:
66
+ """Output as pretty JSON."""
67
+ # For single-item results, output the original response to preserve metadata
68
+ output = original if isinstance(rows, dict) else rows
69
+ sys.stdout.write(json.dumps(output, indent=2, ensure_ascii=False, default=str) + "\n")
70
+
71
+
72
+ def _render_csv(rows: list[dict]) -> None:
73
+ """Output as CSV."""
74
+ if not rows:
75
+ return
76
+ all_keys = list(dict.fromkeys(k for r in rows for k in r))
77
+ writer = csv.DictWriter(sys.stdout, fieldnames=all_keys, extrasaction="ignore")
78
+ writer.writeheader()
79
+ writer.writerows(rows)
80
+
81
+
82
+ def _render_table(rows: list[dict]) -> None:
83
+ """Output as a rich table."""
84
+ if not rows:
85
+ console.print("[dim]No results.[/dim]")
86
+ return
87
+ all_keys = list(dict.fromkeys(k for r in rows for k in r))
88
+ table = Table(show_header=True, header_style="bold cyan")
89
+ for key in all_keys:
90
+ table.add_column(key)
91
+ for row in rows:
92
+ table.add_row(*[_truncate(str(row.get(k, ""))) for k in all_keys])
93
+ console.print(table)
94
+
95
+
96
+ def _render_markdown(rows: list[dict]) -> None:
97
+ """Output as a markdown table."""
98
+ if not rows:
99
+ print("*No results.*")
100
+ return
101
+ all_keys = list(dict.fromkeys(k for r in rows for k in r))
102
+ header = "| " + " | ".join(all_keys) + " |"
103
+ separator = "| " + " | ".join("---" for _ in all_keys) + " |"
104
+ print(header)
105
+ print(separator)
106
+ for row in rows:
107
+ line = "| " + " | ".join(_truncate(str(row.get(k, "")), 60) for k in all_keys) + " |"
108
+ print(line)
109
+
110
+
111
+ def _truncate(s: str, max_len: int = 80) -> str:
112
+ """Truncate string for display."""
113
+ s = s.replace("\n", " ").replace("\r", "")
114
+ if len(s) > max_len:
115
+ return s[: max_len - 3] + "..."
116
+ return s
@@ -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,27 @@
1
+ scrapebadger_cli/__init__.py,sha256=GcGSMO-8wr22obtXramTwtqh3jOpjnoSEnoFqytqZAE,84
2
+ scrapebadger_cli/client.py,sha256=QacrT6_herGs5zRqmC6yreqpkfHO2FLC--Br8OSw23E,1725
3
+ scrapebadger_cli/config.py,sha256=Gmxbgltd3iQNM94B_m5O_uurGZf8Im2IUz82yh9D1cc,1945
4
+ scrapebadger_cli/main.py,sha256=06QXZ46MfMghpzzSFnGCqjzbzlLmo34rYrcRw5ncDms,1900
5
+ scrapebadger_cli/output.py,sha256=QMG3S7VTX-zsnG55pbxxC_UTnlheGdFlCJNFycGmHM4,3588
6
+ scrapebadger_cli/commands/__init__.py,sha256=gQ6tnU0Rvm0-ESWFUBU-KDl5dpNOpUTG509hXOQQjwY,27
7
+ scrapebadger_cli/commands/amazon.py,sha256=7leKjyOFsXfPni6Mahw4LwxRJsXT51YcaE_GZm1XIqc,7344
8
+ scrapebadger_cli/commands/auth.py,sha256=16omqB1GaEyxP9jzzESDxWXMgkAY92frl5qkIaMDdXw,1696
9
+ scrapebadger_cli/commands/ebay.py,sha256=gR-96H9kcTLSzpaCK_lDWtjTURrPVxXzCcN27WyGh9U,7289
10
+ scrapebadger_cli/commands/google.py,sha256=b8GGkfP1cKL71PwgPwhpq9h5h8n2aJwY19MaVwFEdJg,41740
11
+ scrapebadger_cli/commands/idealista.py,sha256=0dOdN9S3jQf6CpQy89n8I4TDQdJwOyjYv112J2UJ3CM,8326
12
+ scrapebadger_cli/commands/immobiliare.py,sha256=SkSYkLMKYLEXs6lxhrDqv9q5TZCVuHBuU62nWQC0UhA,5272
13
+ scrapebadger_cli/commands/leboncoin.py,sha256=Jk63e5SLSACyrA6fMLAhLhIIFBiIFq7Vr0jnFZO2HT8,5037
14
+ scrapebadger_cli/commands/loopnet.py,sha256=c60YVqXGBzN0-P3FmqhZbmaELowQlYoKn_E0FxQTAxg,3219
15
+ scrapebadger_cli/commands/realtor.py,sha256=0viGe9xRS6v_5YFKpNkwo6qkA8GyEby5CbJ08VXENnE,3259
16
+ scrapebadger_cli/commands/reddit.py,sha256=nd_PQ1pfk-19L9rsVymyib3s0Ln0y-FuWYdUKKM8vAs,4981
17
+ scrapebadger_cli/commands/stream.py,sha256=jexzHWlaLliAY6D5z55HWlHzCyySLRCy2_ptApnZw3M,9191
18
+ scrapebadger_cli/commands/tiktok.py,sha256=w39EdvHVGEztZxTAizXZc3bCrmHREnzwxoJOWT9CCu8,8219
19
+ scrapebadger_cli/commands/twitter.py,sha256=AZXnusdxm-9UC0HW7v-INkKQTMXBqld3SJn7VgRsplg,6968
20
+ scrapebadger_cli/commands/vinted.py,sha256=IkCXzC57ser_9jaWwfIRvKlgi1HNFRxyPiBxO6Kr7mQ,3093
21
+ scrapebadger_cli/commands/web.py,sha256=EL-gmA0LNIzB2VOgKgotGehChIEtjTYU4oQKtotW17g,3891
22
+ scrapebadger_cli/commands/youtube.py,sha256=5u0P2F3CFY47ciKmnwgtxeBwNPZg6Y_WehX3sLYEOko,19224
23
+ scrapebadger_cli/commands/zillow.py,sha256=Ctz-4ZdPZXS8AEVHVaGkqCOtVxftk3bciUPgAb8EMjA,3698
24
+ scrapebadger_cli-0.2.1.dist-info/METADATA,sha256=ftn0jdsPORTLJ-vDqnF6S8bLtTBMQMI1sPLQBzko3No,1184
25
+ scrapebadger_cli-0.2.1.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
26
+ scrapebadger_cli-0.2.1.dist-info/entry_points.txt,sha256=gLnV1skpUPYYALjXxwGtMfpvTdv_E87cxWl77uayw1s,59
27
+ scrapebadger_cli-0.2.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ scrapebadger = scrapebadger_cli.main:app