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,91 @@
|
|
|
1
|
+
"""Realtor 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
|
+
MARKET = typer.Option("us", "--market", "-m", help="Market: us (realtor.com) or ca (realtor.ca)")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@app.command()
|
|
16
|
+
def search(
|
|
17
|
+
location: str = typer.Argument(..., help="Location, e.g. 'Austin, TX', ZIP, 'Toronto, ON'"),
|
|
18
|
+
market: str = MARKET,
|
|
19
|
+
status: str | None = typer.Option(None, "--status", help="for_sale, for_rent, sold, pending"),
|
|
20
|
+
price_min: float | None = typer.Option(None, "--price-min"),
|
|
21
|
+
price_max: float | None = typer.Option(None, "--price-max"),
|
|
22
|
+
beds_min: int | None = typer.Option(None, "--beds-min"),
|
|
23
|
+
baths_min: int | None = typer.Option(None, "--baths-min"),
|
|
24
|
+
sqft_min: int | None = typer.Option(None, "--sqft-min"),
|
|
25
|
+
sqft_max: int | None = typer.Option(None, "--sqft-max"),
|
|
26
|
+
property_type: str | None = typer.Option(
|
|
27
|
+
None, "--property-type", help="CSV: single_family,condos,townhomes,multi_family,mobile,land"
|
|
28
|
+
),
|
|
29
|
+
sort: str | None = typer.Option(
|
|
30
|
+
None, "--sort", help="relevant, newest, price_low, price_high, photo_count"
|
|
31
|
+
),
|
|
32
|
+
page: int = typer.Option(1, "--page", "-p"),
|
|
33
|
+
limit: int | None = typer.Option(None, "--limit", help="Results per page (1-200)"),
|
|
34
|
+
fmt: str = FMT,
|
|
35
|
+
fields: str | None = FIELDS,
|
|
36
|
+
) -> None:
|
|
37
|
+
"""Search real-estate listings on realtor.com (US) or realtor.ca (CA)."""
|
|
38
|
+
data = api_get(
|
|
39
|
+
"/v1/realtor/search",
|
|
40
|
+
{
|
|
41
|
+
"location": location,
|
|
42
|
+
"market": market,
|
|
43
|
+
"status": status,
|
|
44
|
+
"price_min": price_min,
|
|
45
|
+
"price_max": price_max,
|
|
46
|
+
"beds_min": beds_min,
|
|
47
|
+
"baths_min": baths_min,
|
|
48
|
+
"sqft_min": sqft_min,
|
|
49
|
+
"sqft_max": sqft_max,
|
|
50
|
+
"property_type": property_type,
|
|
51
|
+
"sort": sort,
|
|
52
|
+
"page": page,
|
|
53
|
+
"limit": limit,
|
|
54
|
+
},
|
|
55
|
+
)
|
|
56
|
+
render(data, fmt, fields)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@app.command()
|
|
60
|
+
def property(
|
|
61
|
+
property_id: str = typer.Argument(..., help="Realtor property ID"),
|
|
62
|
+
market: str = MARKET,
|
|
63
|
+
fmt: str = FMT,
|
|
64
|
+
fields: str | None = FIELDS,
|
|
65
|
+
) -> None:
|
|
66
|
+
"""Get full detail for a single property by ID."""
|
|
67
|
+
data = api_get(f"/v1/realtor/properties/{property_id}", {"market": market})
|
|
68
|
+
render(data, fmt, fields)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@app.command()
|
|
72
|
+
def autocomplete(
|
|
73
|
+
query: str = typer.Argument(..., help="Partial location text"),
|
|
74
|
+
market: str = MARKET,
|
|
75
|
+
limit: int | None = typer.Option(None, "--limit", help="1-25, default 10"),
|
|
76
|
+
fmt: str = FMT,
|
|
77
|
+
fields: str | None = FIELDS,
|
|
78
|
+
) -> None:
|
|
79
|
+
"""Get location autocomplete suggestions (cities, ZIPs, addresses)."""
|
|
80
|
+
data = api_get("/v1/realtor/autocomplete", {"query": query, "market": market, "limit": limit})
|
|
81
|
+
render(data, fmt, fields)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@app.command()
|
|
85
|
+
def markets(
|
|
86
|
+
fmt: str = FMT,
|
|
87
|
+
fields: str | None = FIELDS,
|
|
88
|
+
) -> None:
|
|
89
|
+
"""List supported Realtor markets (us=realtor.com, ca=realtor.ca)."""
|
|
90
|
+
data = api_get("/v1/realtor/markets")
|
|
91
|
+
render(data, fmt, fields)
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Reddit 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
|
+
SORT = typer.Option("hot", "--sort", "-s", help="Sort: hot, new, top, rising, controversial")
|
|
13
|
+
TIME = typer.Option("all", "--time", "-t", help="Time: hour, day, week, month, year, all")
|
|
14
|
+
LIMIT = typer.Option(25, "--limit", "-l", help="Results per page (1-100)")
|
|
15
|
+
AFTER = typer.Option(None, "--after", help="Pagination cursor")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@app.command()
|
|
19
|
+
def search(
|
|
20
|
+
query: str = typer.Argument(..., help="Search query"),
|
|
21
|
+
subreddit: str | None = typer.Option(None, "--subreddit", "-r", help="Restrict to subreddit"),
|
|
22
|
+
sort: str = typer.Option("relevance", "--sort", "-s"),
|
|
23
|
+
t: str = TIME,
|
|
24
|
+
limit: int = LIMIT,
|
|
25
|
+
after: str | None = AFTER,
|
|
26
|
+
fmt: str = FMT,
|
|
27
|
+
fields: str | None = FIELDS,
|
|
28
|
+
) -> None:
|
|
29
|
+
"""Search Reddit posts."""
|
|
30
|
+
data = api_get(
|
|
31
|
+
"/v1/reddit/search/posts",
|
|
32
|
+
{"q": query, "subreddit": subreddit, "sort": sort, "t": t, "limit": limit, "after": after},
|
|
33
|
+
)
|
|
34
|
+
render(data, fmt, fields)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@app.command()
|
|
38
|
+
def post(
|
|
39
|
+
post_id: str = typer.Argument(..., help="Reddit post ID"),
|
|
40
|
+
fmt: str = FMT,
|
|
41
|
+
fields: str | None = FIELDS,
|
|
42
|
+
) -> None:
|
|
43
|
+
"""Get post details."""
|
|
44
|
+
data = api_get(f"/v1/reddit/posts/{post_id}")
|
|
45
|
+
render(data, fmt, fields)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@app.command()
|
|
49
|
+
def comments(
|
|
50
|
+
post_id: str = typer.Argument(..., help="Reddit post ID"),
|
|
51
|
+
sort: str = typer.Option("confidence", "--sort", "-s"),
|
|
52
|
+
limit: int = LIMIT,
|
|
53
|
+
depth: int | None = typer.Option(None, "--depth", help="Max comment depth (0-10)"),
|
|
54
|
+
fmt: str = FMT,
|
|
55
|
+
fields: str | None = FIELDS,
|
|
56
|
+
) -> None:
|
|
57
|
+
"""Get post comments."""
|
|
58
|
+
data = api_get(
|
|
59
|
+
f"/v1/reddit/posts/{post_id}/comments",
|
|
60
|
+
{"sort": sort, "limit": limit, "depth": depth},
|
|
61
|
+
)
|
|
62
|
+
render(data, fmt, fields)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@app.command()
|
|
66
|
+
def subreddit(
|
|
67
|
+
name: str = typer.Argument(..., help="Subreddit name"),
|
|
68
|
+
fmt: str = FMT,
|
|
69
|
+
fields: str | None = FIELDS,
|
|
70
|
+
) -> None:
|
|
71
|
+
"""Get subreddit details."""
|
|
72
|
+
data = api_get(f"/v1/reddit/subreddits/{name}")
|
|
73
|
+
render(data, fmt, fields)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@app.command(name="subreddit-posts")
|
|
77
|
+
def subreddit_posts(
|
|
78
|
+
name: str = typer.Argument(..., help="Subreddit name"),
|
|
79
|
+
sort: str = SORT,
|
|
80
|
+
t: str = TIME,
|
|
81
|
+
limit: int = LIMIT,
|
|
82
|
+
after: str | None = AFTER,
|
|
83
|
+
fmt: str = FMT,
|
|
84
|
+
fields: str | None = FIELDS,
|
|
85
|
+
) -> None:
|
|
86
|
+
"""Get posts from a subreddit."""
|
|
87
|
+
data = api_get(
|
|
88
|
+
f"/v1/reddit/subreddits/{name}/posts",
|
|
89
|
+
{"sort": sort, "t": t, "limit": limit, "after": after},
|
|
90
|
+
)
|
|
91
|
+
render(data, fmt, fields)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@app.command()
|
|
95
|
+
def user(
|
|
96
|
+
username: str = typer.Argument(..., help="Reddit username"),
|
|
97
|
+
fmt: str = FMT,
|
|
98
|
+
fields: str | None = FIELDS,
|
|
99
|
+
) -> None:
|
|
100
|
+
"""Get user profile."""
|
|
101
|
+
data = api_get(f"/v1/reddit/users/{username}")
|
|
102
|
+
render(data, fmt, fields)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@app.command(name="user-posts")
|
|
106
|
+
def user_posts(
|
|
107
|
+
username: str = typer.Argument(..., help="Reddit username"),
|
|
108
|
+
sort: str = typer.Option("new", "--sort", "-s"),
|
|
109
|
+
t: str = TIME,
|
|
110
|
+
limit: int = LIMIT,
|
|
111
|
+
after: str | None = AFTER,
|
|
112
|
+
fmt: str = FMT,
|
|
113
|
+
fields: str | None = FIELDS,
|
|
114
|
+
) -> None:
|
|
115
|
+
"""Get posts by a user."""
|
|
116
|
+
data = api_get(
|
|
117
|
+
f"/v1/reddit/users/{username}/posts",
|
|
118
|
+
{"sort": sort, "t": t, "limit": limit, "after": after},
|
|
119
|
+
)
|
|
120
|
+
render(data, fmt, fields)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@app.command(name="user-comments")
|
|
124
|
+
def user_comments(
|
|
125
|
+
username: str = typer.Argument(..., help="Reddit username"),
|
|
126
|
+
sort: str = typer.Option("new", "--sort", "-s"),
|
|
127
|
+
t: str = TIME,
|
|
128
|
+
limit: int = LIMIT,
|
|
129
|
+
after: str | None = AFTER,
|
|
130
|
+
fmt: str = FMT,
|
|
131
|
+
fields: str | None = FIELDS,
|
|
132
|
+
) -> None:
|
|
133
|
+
"""Get comments by a user."""
|
|
134
|
+
data = api_get(
|
|
135
|
+
f"/v1/reddit/users/{username}/comments",
|
|
136
|
+
{"sort": sort, "t": t, "limit": limit, "after": after},
|
|
137
|
+
)
|
|
138
|
+
render(data, fmt, fields)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@app.command()
|
|
142
|
+
def trending(
|
|
143
|
+
sort: str = SORT,
|
|
144
|
+
t: str = TIME,
|
|
145
|
+
limit: int = LIMIT,
|
|
146
|
+
after: str | None = AFTER,
|
|
147
|
+
fmt: str = FMT,
|
|
148
|
+
fields: str | None = FIELDS,
|
|
149
|
+
) -> None:
|
|
150
|
+
"""Get trending posts from Reddit's front page."""
|
|
151
|
+
data = api_get(
|
|
152
|
+
"/v1/reddit/posts/trending",
|
|
153
|
+
{"sort": sort, "t": t, "limit": limit, "after": after},
|
|
154
|
+
)
|
|
155
|
+
render(data, fmt, fields)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@app.command()
|
|
159
|
+
def domain(
|
|
160
|
+
domain_name: str = typer.Argument(..., help="Domain (e.g. github.com)"),
|
|
161
|
+
sort: str = SORT,
|
|
162
|
+
t: str = TIME,
|
|
163
|
+
limit: int = LIMIT,
|
|
164
|
+
after: str | None = AFTER,
|
|
165
|
+
fmt: str = FMT,
|
|
166
|
+
fields: str | None = FIELDS,
|
|
167
|
+
) -> None:
|
|
168
|
+
"""Get posts linking to a domain."""
|
|
169
|
+
data = api_get(
|
|
170
|
+
f"/v1/reddit/domains/{domain_name}/posts",
|
|
171
|
+
{"sort": sort, "t": t, "limit": limit, "after": after},
|
|
172
|
+
)
|
|
173
|
+
render(data, fmt, fields)
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""Real-time Twitter streaming commands.
|
|
2
|
+
|
|
3
|
+
Connects to ScrapeBadger's WebSocket endpoint and outputs tweets as NDJSON.
|
|
4
|
+
Also provides monitor and filter-rule management subcommands.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import json
|
|
9
|
+
import signal
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
import websockets
|
|
14
|
+
|
|
15
|
+
from scrapebadger_cli.client import api_get, api_post
|
|
16
|
+
from scrapebadger_cli.config import get_api_key
|
|
17
|
+
from scrapebadger_cli.output import console, render
|
|
18
|
+
|
|
19
|
+
app = typer.Typer(no_args_is_help=True)
|
|
20
|
+
|
|
21
|
+
FMT = typer.Option("json", "--output", "-o", help="Output format: json, csv, table, markdown")
|
|
22
|
+
FIELDS = typer.Option(None, "--fields", "-f", help="Comma-separated fields to include")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ── Live stream ─────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@app.command(name="live")
|
|
29
|
+
def stream_live(
|
|
30
|
+
quiet: bool = typer.Option(False, "--quiet", "-q", help="Suppress connection messages"),
|
|
31
|
+
) -> None:
|
|
32
|
+
"""Connect to the real-time tweet stream (WebSocket).
|
|
33
|
+
|
|
34
|
+
Outputs tweets as NDJSON (one JSON object per line) to stdout.
|
|
35
|
+
Press Ctrl+C to disconnect.
|
|
36
|
+
|
|
37
|
+
Requires at least one active stream monitor or filter rule.
|
|
38
|
+
"""
|
|
39
|
+
api_key = get_api_key()
|
|
40
|
+
if not api_key:
|
|
41
|
+
console.print("[red]No API key. Run: scrapebadger auth[/red]", highlight=False)
|
|
42
|
+
raise typer.Exit(1)
|
|
43
|
+
|
|
44
|
+
asyncio.run(_stream_ws(api_key, quiet))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
async def _stream_ws(api_key: str, quiet: bool) -> None:
|
|
48
|
+
"""Connect to WebSocket and stream tweets to stdout."""
|
|
49
|
+
import os
|
|
50
|
+
|
|
51
|
+
base = os.environ.get("SCRAPEBADGER_API_URL", "https://scrapebadger.com")
|
|
52
|
+
ws_url = base.replace("https://", "wss://").replace("http://", "ws://")
|
|
53
|
+
ws_url = f"{ws_url}/v1/twitter/stream?api_key={api_key}"
|
|
54
|
+
|
|
55
|
+
stop = asyncio.Event()
|
|
56
|
+
|
|
57
|
+
def _signal_handler() -> None:
|
|
58
|
+
stop.set()
|
|
59
|
+
|
|
60
|
+
loop = asyncio.get_event_loop()
|
|
61
|
+
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
62
|
+
try:
|
|
63
|
+
loop.add_signal_handler(sig, _signal_handler)
|
|
64
|
+
except NotImplementedError:
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
if not quiet:
|
|
68
|
+
console.print("[dim]Connecting to tweet stream...[/dim]", highlight=False)
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
async for ws in websockets.connect(ws_url, ping_interval=30, ping_timeout=10):
|
|
72
|
+
try:
|
|
73
|
+
if not quiet:
|
|
74
|
+
console.print(
|
|
75
|
+
"[green]Connected.[/green] Waiting for tweets... (Ctrl+C to stop)",
|
|
76
|
+
highlight=False,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
async for raw in ws:
|
|
80
|
+
if stop.is_set():
|
|
81
|
+
break
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
msg = json.loads(raw)
|
|
85
|
+
except json.JSONDecodeError:
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
msg_type = msg.get("type", "")
|
|
89
|
+
|
|
90
|
+
if msg_type == "connected":
|
|
91
|
+
if not quiet:
|
|
92
|
+
console.print(
|
|
93
|
+
f"[dim]Session: {msg.get('connection_id', '?')[:8]}...[/dim]",
|
|
94
|
+
highlight=False,
|
|
95
|
+
)
|
|
96
|
+
continue
|
|
97
|
+
|
|
98
|
+
if msg_type == "error":
|
|
99
|
+
console.print(
|
|
100
|
+
f"[red]Stream error: {msg.get('message', 'unknown')}[/red]",
|
|
101
|
+
highlight=False,
|
|
102
|
+
)
|
|
103
|
+
raise typer.Exit(1)
|
|
104
|
+
|
|
105
|
+
if msg_type in ("tweet", "filter_rule_tweet"):
|
|
106
|
+
# Output as NDJSON line to stdout (pipe-friendly)
|
|
107
|
+
sys.stdout.write(json.dumps(msg, ensure_ascii=False) + "\n")
|
|
108
|
+
sys.stdout.flush()
|
|
109
|
+
elif msg_type == "ping":
|
|
110
|
+
await ws.send(json.dumps({"type": "pong"}))
|
|
111
|
+
|
|
112
|
+
if stop.is_set():
|
|
113
|
+
break
|
|
114
|
+
|
|
115
|
+
except websockets.ConnectionClosed:
|
|
116
|
+
if stop.is_set():
|
|
117
|
+
break
|
|
118
|
+
if not quiet:
|
|
119
|
+
console.print(
|
|
120
|
+
"[yellow]Connection lost. Reconnecting...[/yellow]", highlight=False
|
|
121
|
+
)
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
except asyncio.CancelledError:
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
if not quiet:
|
|
128
|
+
console.print("\n[dim]Disconnected.[/dim]", highlight=False)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# ── Monitor management ──────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@app.command(name="monitors")
|
|
135
|
+
def list_monitors(
|
|
136
|
+
status: str | None = typer.Option(None, "--status", "-s", help="Filter: active, paused"),
|
|
137
|
+
fmt: str = FMT,
|
|
138
|
+
fields: str | None = FIELDS,
|
|
139
|
+
) -> None:
|
|
140
|
+
"""List your stream monitors."""
|
|
141
|
+
data = api_get("/v1/twitter/stream/monitors", {"status": status})
|
|
142
|
+
render(data, fmt, fields)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@app.command(name="monitor-create")
|
|
146
|
+
def create_monitor(
|
|
147
|
+
name: str = typer.Argument(..., help="Monitor name/tag"),
|
|
148
|
+
usernames: str = typer.Argument(..., help="Comma-separated Twitter usernames to monitor"),
|
|
149
|
+
webhook_url: str | None = typer.Option(None, "--webhook", help="Webhook URL for deliveries"),
|
|
150
|
+
fmt: str = FMT,
|
|
151
|
+
fields: str | None = FIELDS,
|
|
152
|
+
) -> None:
|
|
153
|
+
"""Create a new stream monitor."""
|
|
154
|
+
body: dict = {
|
|
155
|
+
"name": name,
|
|
156
|
+
"usernames": [u.strip().lstrip("@") for u in usernames.split(",")],
|
|
157
|
+
}
|
|
158
|
+
if webhook_url:
|
|
159
|
+
body["webhook_url"] = webhook_url
|
|
160
|
+
data = api_post("/v1/twitter/stream/monitors", body)
|
|
161
|
+
render(data, fmt, fields)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@app.command(name="monitor-delete")
|
|
165
|
+
def delete_monitor(
|
|
166
|
+
monitor_id: str = typer.Argument(..., help="Monitor ID to delete"),
|
|
167
|
+
) -> None:
|
|
168
|
+
"""Delete a stream monitor."""
|
|
169
|
+
from scrapebadger_cli.client import _get_client
|
|
170
|
+
|
|
171
|
+
resp = _get_client().delete(f"/v1/twitter/stream/monitors/{monitor_id}")
|
|
172
|
+
if resp.status_code == 204:
|
|
173
|
+
console.print("[green]Monitor deleted.[/green]")
|
|
174
|
+
else:
|
|
175
|
+
console.print(f"[red]Error ({resp.status_code}): {resp.text}[/red]")
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@app.command(name="monitor-pause")
|
|
179
|
+
def pause_monitor(
|
|
180
|
+
monitor_id: str = typer.Argument(..., help="Monitor ID"),
|
|
181
|
+
) -> None:
|
|
182
|
+
"""Pause a stream monitor."""
|
|
183
|
+
from scrapebadger_cli.client import _get_client
|
|
184
|
+
|
|
185
|
+
resp = _get_client().patch(
|
|
186
|
+
f"/v1/twitter/stream/monitors/{monitor_id}",
|
|
187
|
+
json={"status": "paused"},
|
|
188
|
+
)
|
|
189
|
+
if resp.status_code < 400:
|
|
190
|
+
console.print("[green]Monitor paused.[/green]")
|
|
191
|
+
else:
|
|
192
|
+
console.print(f"[red]Error ({resp.status_code}): {resp.text}[/red]")
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@app.command(name="monitor-resume")
|
|
196
|
+
def resume_monitor(
|
|
197
|
+
monitor_id: str = typer.Argument(..., help="Monitor ID"),
|
|
198
|
+
) -> None:
|
|
199
|
+
"""Resume a paused stream monitor."""
|
|
200
|
+
from scrapebadger_cli.client import _get_client
|
|
201
|
+
|
|
202
|
+
resp = _get_client().patch(
|
|
203
|
+
f"/v1/twitter/stream/monitors/{monitor_id}",
|
|
204
|
+
json={"status": "active"},
|
|
205
|
+
)
|
|
206
|
+
if resp.status_code < 400:
|
|
207
|
+
console.print("[green]Monitor resumed.[/green]")
|
|
208
|
+
else:
|
|
209
|
+
console.print(f"[red]Error ({resp.status_code}): {resp.text}[/red]")
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
# ── Filter rule management ──────────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@app.command(name="rules")
|
|
216
|
+
def list_rules(
|
|
217
|
+
status: str | None = typer.Option(None, "--status", "-s", help="Filter: active, paused"),
|
|
218
|
+
fmt: str = FMT,
|
|
219
|
+
fields: str | None = FIELDS,
|
|
220
|
+
) -> None:
|
|
221
|
+
"""List your filter rules."""
|
|
222
|
+
data = api_get("/v1/twitter/filter-rules", {"status": status})
|
|
223
|
+
render(data, fmt, fields)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@app.command(name="rule-create")
|
|
227
|
+
def create_rule(
|
|
228
|
+
tag: str = typer.Argument(..., help="Unique label for the rule"),
|
|
229
|
+
query: str = typer.Argument(..., help="Twitter search query"),
|
|
230
|
+
interval: float = typer.Option(60.0, "--interval", "-i", help="Polling interval in seconds"),
|
|
231
|
+
webhook_url: str | None = typer.Option(None, "--webhook", help="Webhook URL"),
|
|
232
|
+
fmt: str = FMT,
|
|
233
|
+
fields: str | None = FIELDS,
|
|
234
|
+
) -> None:
|
|
235
|
+
"""Create a new filter rule for real-time tweet monitoring."""
|
|
236
|
+
body: dict = {
|
|
237
|
+
"tag": tag,
|
|
238
|
+
"query": query,
|
|
239
|
+
"interval_seconds": interval,
|
|
240
|
+
}
|
|
241
|
+
if webhook_url:
|
|
242
|
+
body["webhook_url"] = webhook_url
|
|
243
|
+
data = api_post("/v1/twitter/filter-rules", body)
|
|
244
|
+
render(data, fmt, fields)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
@app.command(name="rule-delete")
|
|
248
|
+
def delete_rule(
|
|
249
|
+
rule_id: str = typer.Argument(..., help="Filter rule ID to delete"),
|
|
250
|
+
) -> None:
|
|
251
|
+
"""Delete a filter rule."""
|
|
252
|
+
from scrapebadger_cli.client import _get_client
|
|
253
|
+
|
|
254
|
+
resp = _get_client().delete(f"/v1/twitter/filter-rules/{rule_id}")
|
|
255
|
+
if resp.status_code == 204:
|
|
256
|
+
console.print("[green]Filter rule deleted.[/green]")
|
|
257
|
+
else:
|
|
258
|
+
console.print(f"[red]Error ({resp.status_code}): {resp.text}[/red]")
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@app.command(name="delivery-logs")
|
|
262
|
+
def delivery_logs(
|
|
263
|
+
page: int = typer.Option(1, "--page"),
|
|
264
|
+
page_size: int = typer.Option(20, "--page-size"),
|
|
265
|
+
fmt: str = FMT,
|
|
266
|
+
fields: str | None = FIELDS,
|
|
267
|
+
) -> None:
|
|
268
|
+
"""List recent tweet delivery logs."""
|
|
269
|
+
data = api_get("/v1/twitter/stream/logs", {"page": page, "page_size": page_size})
|
|
270
|
+
render(data, fmt, fields)
|