reddipy 1.0.0__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.
reddipy/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ __version__ = "1.0.0"
2
+
3
+ from reddipy.sdk import Reddit
4
+ from reddipy.models import Post, Comment, UserActivity
reddipy/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from reddipy.cli import main
2
+
3
+ main()
reddipy/cli.py ADDED
@@ -0,0 +1,238 @@
1
+ """CLI entry point for RedMiner."""
2
+
3
+ import argparse
4
+ import json
5
+ import sys
6
+
7
+ from rich.console import Console
8
+
9
+ from reddipy.client import RedditClient
10
+ from reddipy.config import get_cookie, set_cookie, clear_config
11
+ from reddipy.exceptions import (
12
+ NetworkError,
13
+ PostNotFoundError,
14
+ ProxyError,
15
+ RateLimitError,
16
+ RedMinerError,
17
+ SubredditNotFoundError,
18
+ UserNotFoundError,
19
+ )
20
+ from reddipy.exporters import ConsoleExporter, CSVExporter, JSONExporter
21
+ from reddipy.scrapers import PostScraper, SearchScraper, SubredditScraper, UserScraper
22
+ from reddipy.utils import generate_filename
23
+
24
+ console = Console(stderr=True)
25
+
26
+
27
+ def _add_common_flags(parser: argparse.ArgumentParser) -> None:
28
+ """Add flags shared by all subcommands."""
29
+ parser.add_argument("--limit", type=int, default=25)
30
+ parser.add_argument("--time", "-t", default="all",
31
+ choices=["day", "week", "month", "year", "all"])
32
+ parser.add_argument("--export", default="console",
33
+ choices=["json", "csv", "console"])
34
+ parser.add_argument("--output", "-o", default=None)
35
+ parser.add_argument("--proxy", default=None)
36
+ parser.add_argument("--proxy-file", default=None)
37
+ parser.add_argument("--cookie", default=None,
38
+ help="Reddit session cookie (overrides stored config)")
39
+ parser.add_argument("--cookie-file", default=None,
40
+ help="Path to JSON file with cookies")
41
+ parser.add_argument("--delay", type=float, default=2.0)
42
+ parser.add_argument("--user-agent", default=None)
43
+ parser.add_argument("--verbose", "-v", action="store_true", default=False)
44
+
45
+
46
+ def build_parser() -> argparse.ArgumentParser:
47
+ """Build the argument parser with all subcommands."""
48
+ parser = argparse.ArgumentParser(
49
+ prog="reddipy",
50
+ description="RedMiner - Scrape Reddit data from the command line.",
51
+ )
52
+ subparsers = parser.add_subparsers(dest="command")
53
+
54
+ # configure
55
+ config_parser = subparsers.add_parser("configure",
56
+ help="Save Reddit session cookie for persistent use")
57
+ config_parser.add_argument("cookie", type=str,
58
+ help="Your reddit_session cookie value")
59
+
60
+ # logout
61
+ subparsers.add_parser("logout", help="Remove stored authentication")
62
+
63
+ # search
64
+ search_parser = subparsers.add_parser("search", help="Search Reddit posts")
65
+ search_parser.add_argument("query", type=str)
66
+ search_parser.add_argument("--sort", default="relevance")
67
+ search_parser.add_argument("--subreddit", "-s", default=None)
68
+ _add_common_flags(search_parser)
69
+
70
+ # post
71
+ post_parser = subparsers.add_parser("post", help="Fetch a single post and its comments")
72
+ post_parser.add_argument("url", type=str)
73
+ post_parser.add_argument("--sort", default="best")
74
+ _add_common_flags(post_parser)
75
+
76
+ # subreddit
77
+ sub_parser = subparsers.add_parser("subreddit", help="Fetch posts from a subreddit")
78
+ sub_parser.add_argument("name", type=str)
79
+ sub_parser.add_argument("--sort", default="hot")
80
+ _add_common_flags(sub_parser)
81
+
82
+ # user
83
+ user_parser = subparsers.add_parser("user", help="Fetch user activity")
84
+ user_parser.add_argument("username", type=str)
85
+ user_parser.add_argument("--sort", default="new")
86
+ user_parser.add_argument("--type", default="all",
87
+ choices=["posts", "comments", "all"])
88
+ _add_common_flags(user_parser)
89
+
90
+ return parser
91
+
92
+
93
+ def _resolve_cookie(args: argparse.Namespace) -> str | None:
94
+ """Resolve cookie from args or stored config."""
95
+ if getattr(args, "cookie", None):
96
+ return args.cookie
97
+ if getattr(args, "cookie_file", None):
98
+ return None # client handles cookie_file directly
99
+ return get_cookie()
100
+
101
+
102
+ def handle_search(client: RedditClient, args: argparse.Namespace) -> list:
103
+ scraper = SearchScraper(client)
104
+ return scraper.scrape(
105
+ args.query,
106
+ sort=args.sort,
107
+ time_filter=args.time,
108
+ limit=args.limit,
109
+ subreddit=getattr(args, "subreddit", None),
110
+ )
111
+
112
+
113
+ def handle_post(client: RedditClient, args: argparse.Namespace) -> tuple:
114
+ scraper = PostScraper(client)
115
+ post, comments = scraper.scrape(args.url)
116
+ return (post, comments)
117
+
118
+
119
+ def handle_subreddit(client: RedditClient, args: argparse.Namespace) -> list:
120
+ scraper = SubredditScraper(client)
121
+ return scraper.scrape(
122
+ args.name,
123
+ sort=args.sort,
124
+ time_filter=args.time,
125
+ limit=args.limit,
126
+ )
127
+
128
+
129
+ def handle_user(client: RedditClient, args: argparse.Namespace) -> list:
130
+ scraper = UserScraper(client)
131
+ return scraper.scrape(
132
+ args.username,
133
+ limit=args.limit,
134
+ sort=args.sort,
135
+ activity_type=args.type,
136
+ )
137
+
138
+
139
+ def export_data(data, args: argparse.Namespace) -> None:
140
+ """Export scraped data using the selected exporter."""
141
+ if args.export == "json":
142
+ exporter = JSONExporter()
143
+ ext = "json"
144
+ elif args.export == "csv":
145
+ exporter = CSVExporter()
146
+ ext = "csv"
147
+ else:
148
+ exporter = ConsoleExporter()
149
+ ext = None
150
+
151
+ output_path = getattr(args, "output", None)
152
+ if output_path is None and ext is not None:
153
+ output_path = generate_filename(args.command, ext)
154
+
155
+ if isinstance(data, tuple):
156
+ post, comments = data
157
+ if args.export == "console":
158
+ console.print("\n[bold]Post[/bold]")
159
+ exporter.export([post])
160
+ if comments:
161
+ console.print(f"\n[bold]Comments[/bold] ({len(comments)})")
162
+ exporter.export(comments)
163
+ elif args.export == "json":
164
+ combined = {
165
+ "post": post.to_dict(),
166
+ "comments": [c.to_dict() for c in comments],
167
+ }
168
+ if output_path:
169
+ with open(output_path, "w", encoding="utf-8") as f:
170
+ json.dump(combined, f, indent=2)
171
+ console.print(f"[green]Saved to {output_path}[/green]")
172
+ else:
173
+ print(json.dumps(combined, indent=2))
174
+ else:
175
+ if comments:
176
+ exporter.export(comments, output_path)
177
+ else:
178
+ exporter.export([post], output_path)
179
+ if output_path:
180
+ console.print(f"[green]Saved to {output_path}[/green]")
181
+ else:
182
+ exporter.export(data, output_path)
183
+ if output_path and ext:
184
+ console.print(f"[green]Saved to {output_path}[/green]")
185
+
186
+
187
+ def main() -> None:
188
+ """CLI entry point."""
189
+ parser = build_parser()
190
+ args = parser.parse_args()
191
+
192
+ if not hasattr(args, "command") or args.command is None:
193
+ parser.print_help()
194
+ sys.exit(1)
195
+
196
+ # Handle configure/logout commands
197
+ if args.command == "configure":
198
+ set_cookie(args.cookie)
199
+ console.print("[green]Cookie saved successfully.[/green] You can now use reddipy without --cookie.")
200
+ return
201
+
202
+ if args.command == "logout":
203
+ clear_config()
204
+ console.print("[yellow]Stored authentication removed.[/yellow]")
205
+ return
206
+
207
+ try:
208
+ cookie = _resolve_cookie(args)
209
+ client = RedditClient(
210
+ delay=args.delay,
211
+ proxy=args.proxy,
212
+ proxy_file=args.proxy_file,
213
+ user_agent=args.user_agent,
214
+ verbose=args.verbose,
215
+ cookie=cookie,
216
+ cookie_file=getattr(args, "cookie_file", None),
217
+ )
218
+
219
+ if args.command == "search":
220
+ data = handle_search(client, args)
221
+ elif args.command == "post":
222
+ data = handle_post(client, args)
223
+ elif args.command == "subreddit":
224
+ data = handle_subreddit(client, args)
225
+ elif args.command == "user":
226
+ data = handle_user(client, args)
227
+
228
+ export_data(data, args)
229
+
230
+ except (SubredditNotFoundError, PostNotFoundError, UserNotFoundError) as e:
231
+ console.print(f"[red]Error:[/red] {e}")
232
+ sys.exit(3)
233
+ except (NetworkError, RateLimitError, ProxyError) as e:
234
+ console.print(f"[red]Error:[/red] {e}")
235
+ sys.exit(2)
236
+ except RedMinerError as e:
237
+ console.print(f"[red]Error:[/red] {e}")
238
+ sys.exit(1)
reddipy/client.py ADDED
@@ -0,0 +1,254 @@
1
+ """RedditClient: HTTP layer with rate limiting, pagination, and proxy support."""
2
+
3
+ import random
4
+ import sys
5
+ import time
6
+
7
+ import requests
8
+
9
+ from reddipy.exceptions import (
10
+ NetworkError,
11
+ PostNotFoundError,
12
+ ProxyError,
13
+ RateLimitError,
14
+ SubredditNotFoundError,
15
+ UserNotFoundError,
16
+ )
17
+
18
+ DEFAULT_USER_AGENT = (
19
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
20
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
21
+ "Chrome/149.0.0.0 Safari/537.36"
22
+ )
23
+
24
+ DEFAULT_HEADERS = {
25
+ "User-Agent": DEFAULT_USER_AGENT,
26
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
27
+ "Accept-Language": "en-US,en;q=0.9",
28
+ "Accept-Encoding": "gzip, deflate, br",
29
+ "Cache-Control": "no-cache",
30
+ "Pragma": "no-cache",
31
+ "Sec-CH-UA": '"Google Chrome";v="149", "Chromium";v="149", "Not)A;Brand";v="24"',
32
+ "Sec-CH-UA-Mobile": "?0",
33
+ "Sec-CH-UA-Platform": '"Windows"',
34
+ "Sec-Fetch-Dest": "document",
35
+ "Sec-Fetch-Mode": "navigate",
36
+ "Sec-Fetch-Site": "none",
37
+ "Sec-Fetch-User": "?1",
38
+ "Upgrade-Insecure-Requests": "1",
39
+ "Connection": "keep-alive",
40
+ }
41
+
42
+ MAX_RETRIES = 3
43
+ BACKOFF_SCHEDULE = [2, 4, 8]
44
+ DEAD_PROXY_COOLDOWN = 60 # seconds
45
+
46
+
47
+ class RedditClient:
48
+ """HTTP client for Reddit's public JSON endpoints."""
49
+
50
+ BASE_URL = "https://www.reddit.com"
51
+
52
+ def __init__(
53
+ self,
54
+ delay: float = 2.0,
55
+ proxy: str | None = None,
56
+ proxy_file: str | None = None,
57
+ user_agent: str | None = None,
58
+ verbose: bool = False,
59
+ cookie: str | None = None,
60
+ cookie_file: str | None = None,
61
+ ):
62
+ self.delay = delay
63
+ self.verbose = verbose
64
+ self._headers = dict(DEFAULT_HEADERS)
65
+ if user_agent:
66
+ self._headers["User-Agent"] = user_agent
67
+ self._last_request_time: float = 0.0
68
+ self._dead_proxies: dict[str, float] = {}
69
+
70
+ # Build proxy pool
71
+ self._proxies: list[str] = []
72
+ if proxy:
73
+ self._proxies = [proxy]
74
+ elif proxy_file:
75
+ self._proxies = self._load_proxy_file(proxy_file)
76
+
77
+ # Build cookie jar
78
+ self._cookies: dict[str, str] = {}
79
+ if cookie:
80
+ self._cookies["reddit_session"] = cookie
81
+ elif cookie_file:
82
+ self._cookies = self._load_cookie_file(cookie_file)
83
+
84
+ # ------------------------------------------------------------------
85
+ # Public API
86
+ # ------------------------------------------------------------------
87
+
88
+ def fetch(self, url: str, params: dict | None = None) -> dict:
89
+ """Single HTTP GET with rate limiting, retries, and proxy support."""
90
+ self._rate_limit()
91
+
92
+ proxy_dict = self._select_proxy()
93
+
94
+ for attempt in range(MAX_RETRIES + 1):
95
+ try:
96
+ resp = requests.get(
97
+ url,
98
+ headers=self._headers,
99
+ params=params or {},
100
+ proxies=proxy_dict,
101
+ cookies=self._cookies or None,
102
+ timeout=30,
103
+ )
104
+ except (requests.ConnectionError, requests.Timeout) as exc:
105
+ # Mark the proxy dead if one was used
106
+ if proxy_dict:
107
+ proxy_url = proxy_dict.get("http")
108
+ if proxy_url:
109
+ self._dead_proxies[proxy_url] = time.time()
110
+ raise NetworkError(str(exc)) from exc
111
+
112
+ status = resp.status_code
113
+
114
+ # 403 without cookies = auth issue, not "not found"
115
+ if status == 403 and not self._cookies:
116
+ raise NetworkError(
117
+ "Reddit returned 403 Forbidden. You likely need to authenticate. "
118
+ "Run: Reddit.configure(cookie='your_reddit_session') or use --cookie"
119
+ )
120
+
121
+ # Permanent errors (not retryable)
122
+ if status in (403, 404):
123
+ self._raise_not_found(url, status)
124
+
125
+ # Rate limited
126
+ if status == 429:
127
+ if attempt < MAX_RETRIES:
128
+ wait = BACKOFF_SCHEDULE[attempt]
129
+ self._log(f"Rate limited (429). Cooling down {wait}s before retry {attempt + 1}/{MAX_RETRIES}...")
130
+ time.sleep(wait)
131
+ continue
132
+ raise RateLimitError(
133
+ f"Rate limited after {MAX_RETRIES} retries: {url}"
134
+ )
135
+
136
+ # Server errors
137
+ if status >= 500:
138
+ if attempt < MAX_RETRIES:
139
+ wait = BACKOFF_SCHEDULE[attempt]
140
+ self._log(f"Server error ({status}). Cooling down {wait}s before retry {attempt + 1}/{MAX_RETRIES}...")
141
+ time.sleep(BACKOFF_SCHEDULE[attempt])
142
+ continue
143
+ raise NetworkError(
144
+ f"Server error {status} after {MAX_RETRIES} retries: {url}"
145
+ )
146
+
147
+ # Success
148
+ return resp.json()
149
+
150
+ # Should not be reachable, but just in case
151
+ raise NetworkError(f"Unexpected state after retries: {url}") # pragma: no cover
152
+
153
+ def fetch_all(
154
+ self, url: str, params: dict | None = None, limit: int = 25
155
+ ) -> list[dict]:
156
+ """Paginated fetch that collects items across multiple pages."""
157
+ collected: list[dict] = []
158
+ after: str | None = None
159
+ params = dict(params) if params else {}
160
+
161
+ while len(collected) < limit:
162
+ remaining = limit - len(collected)
163
+ page_params = {**params, "limit": min(100, remaining)}
164
+ if after:
165
+ page_params["after"] = after
166
+
167
+ data = self.fetch(url, params=page_params)
168
+ children = data.get("data", {}).get("children", [])
169
+
170
+ for child in children:
171
+ if len(collected) >= limit:
172
+ break
173
+ collected.append(child.get("data", {}))
174
+
175
+ after = data.get("data", {}).get("after")
176
+ if not after:
177
+ break
178
+
179
+ return collected
180
+
181
+ # ------------------------------------------------------------------
182
+ # Proxy management
183
+ # ------------------------------------------------------------------
184
+
185
+ def _select_proxy(self) -> dict | None:
186
+ """Pick a live proxy from the pool, or None if no proxies configured."""
187
+ if not self._proxies:
188
+ return None
189
+
190
+ now = time.time()
191
+ alive = [
192
+ p
193
+ for p in self._proxies
194
+ if p not in self._dead_proxies
195
+ or (now - self._dead_proxies[p]) >= DEAD_PROXY_COOLDOWN
196
+ ]
197
+
198
+ if not alive:
199
+ raise ProxyError("All proxies are dead")
200
+
201
+ chosen = random.choice(alive)
202
+ return {"http": chosen, "https": chosen}
203
+
204
+ # ------------------------------------------------------------------
205
+ # Internal helpers
206
+ # ------------------------------------------------------------------
207
+
208
+ def _rate_limit(self) -> None:
209
+ """Sleep if needed to enforce minimum delay between requests."""
210
+ if self._last_request_time > 0 and self.delay > 0:
211
+ elapsed = time.time() - self._last_request_time
212
+ if elapsed < self.delay:
213
+ time.sleep(self.delay - elapsed)
214
+ self._last_request_time = time.time()
215
+
216
+ def _raise_not_found(self, url: str, status: int) -> None:
217
+ """Raise the appropriate not-found exception based on URL pattern."""
218
+ if "/comments/" in url:
219
+ raise PostNotFoundError(f"Post not found ({status}): {url}")
220
+ if "/user/" in url or "/u/" in url:
221
+ raise UserNotFoundError(f"User not found ({status}): {url}")
222
+ # Default to subreddit
223
+ raise SubredditNotFoundError(f"Subreddit not found ({status}): {url}")
224
+
225
+ def _log(self, message: str) -> None:
226
+ """Print to stderr if verbose mode is on."""
227
+ if self.verbose:
228
+ print(message, file=sys.stderr)
229
+
230
+ @staticmethod
231
+ def _load_proxy_file(path: str) -> list[str]:
232
+ """Load proxy URLs from a file, one per line."""
233
+ with open(path, "r", encoding="utf-8") as f:
234
+ proxies = [line.strip() for line in f if line.strip()]
235
+ return proxies
236
+
237
+ @staticmethod
238
+ def _load_cookie_file(path: str) -> dict[str, str]:
239
+ """Load cookies from a JSON file.
240
+
241
+ Expects either:
242
+ - A JSON object: {"reddit_session": "value", ...}
243
+ - A JSON array (browser export): [{"name": "reddit_session", "value": "..."}, ...]
244
+ """
245
+ import json as _json
246
+
247
+ with open(path, "r", encoding="utf-8") as f:
248
+ data = _json.load(f)
249
+
250
+ if isinstance(data, dict):
251
+ return data
252
+ if isinstance(data, list):
253
+ return {item["name"]: item["value"] for item in data if "name" in item and "value" in item}
254
+ return {}
reddipy/config.py ADDED
@@ -0,0 +1,44 @@
1
+ """Persistent configuration for RedMiner (stored at ~/.reddipy/config.json)."""
2
+
3
+ import json
4
+ import os
5
+
6
+ CONFIG_DIR = os.path.join(os.path.expanduser("~"), ".reddipy")
7
+ CONFIG_FILE = os.path.join(CONFIG_DIR, "config.json")
8
+
9
+
10
+ def _ensure_dir() -> None:
11
+ os.makedirs(CONFIG_DIR, exist_ok=True)
12
+
13
+
14
+ def load_config() -> dict:
15
+ """Load config from ~/.reddipy/config.json. Returns empty dict if missing."""
16
+ if not os.path.exists(CONFIG_FILE):
17
+ return {}
18
+ with open(CONFIG_FILE, "r", encoding="utf-8") as f:
19
+ return json.load(f)
20
+
21
+
22
+ def save_config(config: dict) -> None:
23
+ """Save config to ~/.reddipy/config.json."""
24
+ _ensure_dir()
25
+ with open(CONFIG_FILE, "w", encoding="utf-8") as f:
26
+ json.dump(config, f, indent=2)
27
+
28
+
29
+ def get_cookie() -> str | None:
30
+ """Get stored reddit_session cookie, or None."""
31
+ return load_config().get("reddit_session")
32
+
33
+
34
+ def set_cookie(cookie: str) -> None:
35
+ """Store reddit_session cookie persistently."""
36
+ config = load_config()
37
+ config["reddit_session"] = cookie
38
+ save_config(config)
39
+
40
+
41
+ def clear_config() -> None:
42
+ """Remove stored config."""
43
+ if os.path.exists(CONFIG_FILE):
44
+ os.remove(CONFIG_FILE)
reddipy/exceptions.py ADDED
@@ -0,0 +1,29 @@
1
+ """Domain-specific exceptions for RedMiner."""
2
+
3
+
4
+ class RedMinerError(Exception):
5
+ """Base exception for all RedMiner errors."""
6
+
7
+
8
+ class SubredditNotFoundError(RedMinerError):
9
+ """Raised when a subreddit is private, banned, or nonexistent (403/404)."""
10
+
11
+
12
+ class PostNotFoundError(RedMinerError):
13
+ """Raised when a post URL is deleted or invalid."""
14
+
15
+
16
+ class UserNotFoundError(RedMinerError):
17
+ """Raised when a user is suspended, shadow-banned, or nonexistent."""
18
+
19
+
20
+ class RateLimitError(RedMinerError):
21
+ """Raised when all retries are exhausted after repeated 429 responses."""
22
+
23
+
24
+ class NetworkError(RedMinerError):
25
+ """Raised on connection failures or timeouts."""
26
+
27
+
28
+ class ProxyError(RedMinerError):
29
+ """Raised when all proxies are exhausted or dead."""
@@ -0,0 +1,5 @@
1
+ """Exporter modules for RedMiner."""
2
+
3
+ from reddipy.exporters.json_exporter import JSONExporter
4
+ from reddipy.exporters.csv_exporter import CSVExporter
5
+ from reddipy.exporters.console import ConsoleExporter
@@ -0,0 +1,9 @@
1
+ """Abstract base class for all exporters."""
2
+
3
+ from abc import ABC, abstractmethod
4
+
5
+
6
+ class BaseExporter(ABC):
7
+ @abstractmethod
8
+ def export(self, data: list, output_path: str | None = None) -> None:
9
+ pass
@@ -0,0 +1,96 @@
1
+ """Console exporter: prints rich formatted tables to stdout."""
2
+
3
+ from rich.console import Console
4
+ from rich.table import Table
5
+
6
+ from reddipy.exporters.base import BaseExporter
7
+
8
+
9
+ def _truncate(text: str | None, max_len: int) -> str:
10
+ if text is None:
11
+ return ""
12
+ text = text.replace("\n", " ").replace("\r", "")
13
+ if len(text) <= max_len:
14
+ return text
15
+ return text[:max_len] + "..."
16
+
17
+
18
+ class ConsoleExporter(BaseExporter):
19
+ def export(self, data: list, output_path: str | None = None) -> None:
20
+ if not data:
21
+ return
22
+
23
+ first = data[0]
24
+ class_name = type(first).__name__
25
+
26
+ if class_name == "Post":
27
+ self._print_posts(data)
28
+ elif class_name == "Comment":
29
+ self._print_comments(data)
30
+ elif class_name == "UserActivity":
31
+ self._print_user_activity(data)
32
+
33
+ def _print_posts(self, data: list) -> None:
34
+ table = Table(show_header=True, header_style="bold cyan", border_style="dim",
35
+ pad_edge=False, show_lines=False, expand=True)
36
+ table.add_column("ID", style="dim", no_wrap=True, ratio=1)
37
+ table.add_column("Title", ratio=5)
38
+ table.add_column("Author", style="green", ratio=2)
39
+ table.add_column("Subreddit", style="magenta", ratio=2)
40
+ table.add_column("Score", justify="right", style="yellow", ratio=1)
41
+ table.add_column("Comments", justify="right", style="blue", ratio=1)
42
+
43
+ for item in data:
44
+ table.add_row(
45
+ item.id,
46
+ _truncate(item.title, 62),
47
+ item.author,
48
+ f"r/{item.subreddit}",
49
+ str(item.score),
50
+ str(item.num_comments),
51
+ )
52
+
53
+ Console().print(table)
54
+
55
+ def _print_comments(self, data: list) -> None:
56
+ table = Table(show_header=True, header_style="bold cyan", border_style="dim",
57
+ pad_edge=False, show_lines=False, expand=True)
58
+ table.add_column("ID", style="dim", no_wrap=True, ratio=1)
59
+ table.add_column("Author", style="green", ratio=2)
60
+ table.add_column("Comment", ratio=5)
61
+ table.add_column("Score", justify="right", style="yellow", ratio=1)
62
+ table.add_column("Depth", justify="right", style="blue", ratio=1)
63
+
64
+ for item in data:
65
+ table.add_row(
66
+ item.id,
67
+ item.author,
68
+ _truncate(item.body, 52),
69
+ str(item.score),
70
+ str(item.depth),
71
+ )
72
+
73
+ Console().print(table)
74
+
75
+ def _print_user_activity(self, data: list) -> None:
76
+ table = Table(show_header=True, header_style="bold cyan", border_style="dim",
77
+ pad_edge=False, show_lines=False, expand=True)
78
+ table.add_column("Type", style="bold", ratio=1)
79
+ table.add_column("ID", style="dim", no_wrap=True, ratio=1)
80
+ table.add_column("Author", style="green", ratio=2)
81
+ table.add_column("Subreddit", style="magenta", ratio=2)
82
+ table.add_column("Score", justify="right", style="yellow", ratio=1)
83
+ table.add_column("Content", ratio=4)
84
+
85
+ for item in data:
86
+ content = item.title if item.title is not None else item.body
87
+ table.add_row(
88
+ item.type,
89
+ item.id,
90
+ item.author,
91
+ f"r/{item.subreddit}",
92
+ str(item.score),
93
+ _truncate(content, 47),
94
+ )
95
+
96
+ Console().print(table)