rdt-cli 0.3.2__tar.gz → 0.4.0__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.
Files changed (40) hide show
  1. {rdt_cli-0.3.2 → rdt_cli-0.4.0}/PKG-INFO +7 -3
  2. {rdt_cli-0.3.2 → rdt_cli-0.4.0}/README.md +6 -2
  3. {rdt_cli-0.3.2 → rdt_cli-0.4.0}/SKILL.md +16 -1
  4. {rdt_cli-0.3.2 → rdt_cli-0.4.0}/pyproject.toml +1 -1
  5. rdt_cli-0.4.0/rdt_cli/__init__.py +3 -0
  6. {rdt_cli-0.3.2 → rdt_cli-0.4.0}/rdt_cli/client.py +68 -0
  7. {rdt_cli-0.3.2 → rdt_cli-0.4.0}/rdt_cli/commands/browse.py +30 -8
  8. {rdt_cli-0.3.2 → rdt_cli-0.4.0}/rdt_cli/constants.py +1 -0
  9. {rdt_cli-0.3.2 → rdt_cli-0.4.0}/tests/test_cli.py +59 -2
  10. {rdt_cli-0.3.2 → rdt_cli-0.4.0}/tests/test_smoke.py +14 -0
  11. {rdt_cli-0.3.2 → rdt_cli-0.4.0}/uv.lock +1 -1
  12. rdt_cli-0.3.2/rdt_cli/__init__.py +0 -3
  13. {rdt_cli-0.3.2 → rdt_cli-0.4.0}/.github/workflows/ci.yml +0 -0
  14. {rdt_cli-0.3.2 → rdt_cli-0.4.0}/.github/workflows/publish.yml +0 -0
  15. {rdt_cli-0.3.2 → rdt_cli-0.4.0}/.gitignore +0 -0
  16. {rdt_cli-0.3.2 → rdt_cli-0.4.0}/SCHEMA.md +0 -0
  17. {rdt_cli-0.3.2 → rdt_cli-0.4.0}/rdt_cli/__main__.py +0 -0
  18. {rdt_cli-0.3.2 → rdt_cli-0.4.0}/rdt_cli/auth.py +0 -0
  19. {rdt_cli-0.3.2 → rdt_cli-0.4.0}/rdt_cli/cli.py +0 -0
  20. {rdt_cli-0.3.2 → rdt_cli-0.4.0}/rdt_cli/commands/__init__.py +0 -0
  21. {rdt_cli-0.3.2 → rdt_cli-0.4.0}/rdt_cli/commands/_common.py +0 -0
  22. {rdt_cli-0.3.2 → rdt_cli-0.4.0}/rdt_cli/commands/auth.py +0 -0
  23. {rdt_cli-0.3.2 → rdt_cli-0.4.0}/rdt_cli/commands/post.py +0 -0
  24. {rdt_cli-0.3.2 → rdt_cli-0.4.0}/rdt_cli/commands/search.py +0 -0
  25. {rdt_cli-0.3.2 → rdt_cli-0.4.0}/rdt_cli/commands/social.py +0 -0
  26. {rdt_cli-0.3.2 → rdt_cli-0.4.0}/rdt_cli/config.py +0 -0
  27. {rdt_cli-0.3.2 → rdt_cli-0.4.0}/rdt_cli/exceptions.py +0 -0
  28. {rdt_cli-0.3.2 → rdt_cli-0.4.0}/rdt_cli/fingerprint.py +0 -0
  29. {rdt_cli-0.3.2 → rdt_cli-0.4.0}/rdt_cli/index_cache.py +0 -0
  30. {rdt_cli-0.3.2 → rdt_cli-0.4.0}/rdt_cli/models.py +0 -0
  31. {rdt_cli-0.3.2 → rdt_cli-0.4.0}/rdt_cli/parser.py +0 -0
  32. {rdt_cli-0.3.2 → rdt_cli-0.4.0}/rdt_cli/session.py +0 -0
  33. {rdt_cli-0.3.2 → rdt_cli-0.4.0}/rdt_cli/transports.py +0 -0
  34. {rdt_cli-0.3.2 → rdt_cli-0.4.0}/tests/__init__.py +0 -0
  35. {rdt_cli-0.3.2 → rdt_cli-0.4.0}/tests/fixtures/listing.json +0 -0
  36. {rdt_cli-0.3.2 → rdt_cli-0.4.0}/tests/fixtures/morechildren.json +0 -0
  37. {rdt_cli-0.3.2 → rdt_cli-0.4.0}/tests/fixtures/post_detail.json +0 -0
  38. {rdt_cli-0.3.2 → rdt_cli-0.4.0}/tests/test_client.py +0 -0
  39. {rdt_cli-0.3.2 → rdt_cli-0.4.0}/tests/test_parser.py +0 -0
  40. {rdt_cli-0.3.2 → rdt_cli-0.4.0}/tests/test_session.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rdt-cli
3
- Version: 0.3.2
3
+ Version: 0.4.0
4
4
  Summary: A CLI for Reddit — browse feeds, read posts, search, and interact via terminal 📖
5
5
  Project-URL: Homepage, https://github.com/jackwener/rdt-cli
6
6
  Project-URL: Repository, https://github.com/jackwener/rdt-cli
@@ -42,7 +42,7 @@ A CLI for Reddit — browse feeds, read posts, search, and interact via reverse-
42
42
  ## Features
43
43
 
44
44
  - 🔐 **Auth** — auto-extract browser cookies, status check, whoami
45
- - 🏠 **Feed** — browse home feed, popular, and /r/all
45
+ - 🏠 **Feed** — browse home feed, popular, /r/all, and subscription-only feed (`--subs-only`)
46
46
  - 📋 **Subreddits** — browse any subreddit with sort/time filters, view subreddit info
47
47
  - 📰 **Posts** — read posts and comment trees with syntax highlighting
48
48
  - 💬 **Expanded comments** — `--expand-more` loads additional `more comments` entries
@@ -95,6 +95,8 @@ rdt logout # Clear saved cookies
95
95
 
96
96
  # ─── Browse ───────────────────────────────────────
97
97
  rdt feed # Home feed (requires login)
98
+ rdt feed --subs-only # Subscriptions-only feed (no algorithm)
99
+ rdt feed --subs-only -n 5 --max-subs 10 # Limit per-sub posts and max subs
98
100
  rdt popular # Popular posts
99
101
  rdt popular --full-text # Show full titles
100
102
  rdt all # /r/all
@@ -294,7 +296,7 @@ The built-in Gaussian jitter delay (~1s between requests) is intentional to mimi
294
296
  ## 功能特性
295
297
 
296
298
  - 🔐 **认证** — 自动提取浏览器 Cookie,状态检查,用户信息
297
- - 🏠 **浏览** — 首页 Feed、Popular、/r/all
299
+ - 🏠 **浏览** — 首页 Feed、Popular、/r/all、纯订阅 Feed(`--subs-only`)
298
300
  - 📋 **子版块** — 浏览任意 subreddit(排序/时间过滤),查看子版块信息
299
301
  - 📰 **帖子** — 阅读帖子和评论树
300
302
  - 💬 **评论展开** — `--expand-more` 可展开额外评论
@@ -343,6 +345,8 @@ rdt logout # 清除缓存的 Cookie
343
345
 
344
346
  # 浏览
345
347
  rdt feed # 首页 Feed(需要登录)
348
+ rdt feed --subs-only # 纯订阅 Feed(无算法推荐)
349
+ rdt feed --subs-only -n 5 --max-subs 10 # 限制每个 sub 帖子数和最大 sub 数
346
350
  rdt popular # 热门帖子
347
351
  rdt all # /r/all
348
352
  rdt sub python # 浏览子版块
@@ -19,7 +19,7 @@ A CLI for Reddit — browse feeds, read posts, search, and interact via reverse-
19
19
  ## Features
20
20
 
21
21
  - 🔐 **Auth** — auto-extract browser cookies, status check, whoami
22
- - 🏠 **Feed** — browse home feed, popular, and /r/all
22
+ - 🏠 **Feed** — browse home feed, popular, /r/all, and subscription-only feed (`--subs-only`)
23
23
  - 📋 **Subreddits** — browse any subreddit with sort/time filters, view subreddit info
24
24
  - 📰 **Posts** — read posts and comment trees with syntax highlighting
25
25
  - 💬 **Expanded comments** — `--expand-more` loads additional `more comments` entries
@@ -72,6 +72,8 @@ rdt logout # Clear saved cookies
72
72
 
73
73
  # ─── Browse ───────────────────────────────────────
74
74
  rdt feed # Home feed (requires login)
75
+ rdt feed --subs-only # Subscriptions-only feed (no algorithm)
76
+ rdt feed --subs-only -n 5 --max-subs 10 # Limit per-sub posts and max subs
75
77
  rdt popular # Popular posts
76
78
  rdt popular --full-text # Show full titles
77
79
  rdt all # /r/all
@@ -271,7 +273,7 @@ The built-in Gaussian jitter delay (~1s between requests) is intentional to mimi
271
273
  ## 功能特性
272
274
 
273
275
  - 🔐 **认证** — 自动提取浏览器 Cookie,状态检查,用户信息
274
- - 🏠 **浏览** — 首页 Feed、Popular、/r/all
276
+ - 🏠 **浏览** — 首页 Feed、Popular、/r/all、纯订阅 Feed(`--subs-only`)
275
277
  - 📋 **子版块** — 浏览任意 subreddit(排序/时间过滤),查看子版块信息
276
278
  - 📰 **帖子** — 阅读帖子和评论树
277
279
  - 💬 **评论展开** — `--expand-more` 可展开额外评论
@@ -320,6 +322,8 @@ rdt logout # 清除缓存的 Cookie
320
322
 
321
323
  # 浏览
322
324
  rdt feed # 首页 Feed(需要登录)
325
+ rdt feed --subs-only # 纯订阅 Feed(无算法推荐)
326
+ rdt feed --subs-only -n 5 --max-subs 10 # 限制每个 sub 帖子数和最大 sub 数
323
327
  rdt popular # 热门帖子
324
328
  rdt all # /r/all
325
329
  rdt sub python # 浏览子版块
@@ -2,7 +2,7 @@
2
2
  name: rdt-cli
3
3
  description: Use rdt-cli for ALL Reddit operations — browsing feeds, reading posts, searching, viewing users, upvoting, saving, and subscribing. Invoke whenever the user requests any Reddit interaction.
4
4
  author: jackwener
5
- version: "0.2.0"
5
+ version: "0.4.0"
6
6
  tags:
7
7
  - reddit
8
8
  - rdt
@@ -82,6 +82,7 @@ Payloads live under `.data`.
82
82
  | Command | Description | Example |
83
83
  |---------|-------------|---------|
84
84
  | `rdt feed` | Browse home feed (requires login) | `rdt feed -n 10 --json` |
85
+ | `rdt feed --subs-only` | Subscriptions-only feed (no algorithm, sorted by time) | `rdt feed --subs-only -n 5 --json` |
85
86
  | `rdt popular` | Browse /r/popular | `rdt popular -n 5 --json` |
86
87
  | `rdt all` | Browse /r/all | `rdt all -n 10 --compact --json` |
87
88
  | `rdt sub <name>` | Browse a subreddit | `rdt sub python -s top -t week` |
@@ -146,6 +147,13 @@ All listing commands (feed, popular, all, sub, user-posts, user-comments, saved,
146
147
  | `--full-text` | Show full title without truncation |
147
148
  | `-c, --compact` | Agent-friendly compact output (fewer fields) |
148
149
 
150
+ ### Feed-specific Options
151
+
152
+ | Flag | Description |
153
+ |------|-------------|
154
+ | `--subs-only` | Show only posts from subscribed subreddits (sorted by time, no algorithm) |
155
+ | `--max-subs N` | Max subscriptions to fetch (default: 20) |
156
+
149
157
  ## Agent Workflow Examples
150
158
 
151
159
  ### Browse → Read → Upvote pipeline
@@ -185,6 +193,13 @@ rdt saved -n 20 --compact --json
185
193
  rdt upvoted -n 20 --compact --json
186
194
  ```
187
195
 
196
+ ### Subscriptions-only monitoring
197
+
198
+ ```bash
199
+ rdt feed --subs-only -n 5 --compact --json
200
+ rdt feed --subs-only --max-subs 10 -o subs_feed.json
201
+ ```
202
+
188
203
  ### Subreddit discovery
189
204
 
190
205
  ```bash
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "rdt-cli"
7
- version = "0.3.2"
7
+ version = "0.4.0"
8
8
  description = "A CLI for Reddit — browse feeds, read posts, search, and interact via terminal 📖"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -0,0 +1,3 @@
1
+ """Reddit CLI."""
2
+
3
+ __version__ = "0.4.0"
@@ -20,6 +20,7 @@ from .constants import (
20
20
  SUBREDDIT_ABOUT_URL,
21
21
  SUBREDDIT_SEARCH_URL,
22
22
  SUBSCRIBE_URL,
23
+ SUBSCRIPTIONS_URL,
23
24
  UNSAVE_URL,
24
25
  USER_ABOUT_URL,
25
26
  USER_COMMENTS_URL,
@@ -349,3 +350,70 @@ class RedditClient:
349
350
  def post_comment(self, parent_fullname: str, text: str) -> dict:
350
351
  """Post a comment."""
351
352
  return self._post(COMMENT_URL, data={"parent": parent_fullname, "text": text})
353
+
354
+ # ── Subscription feed ───────────────────────────────────────────
355
+
356
+ def get_my_subscriptions(
357
+ self, limit: int = 100, max_subs: int = 20,
358
+ ) -> list[str]:
359
+ """Get names of subscribed subreddits (up to max_subs)."""
360
+ names: list[str] = []
361
+ after: str | None = None
362
+ while len(names) < max_subs:
363
+ params: dict[str, Any] = {"limit": min(limit, 100), "raw_json": 1}
364
+ if after:
365
+ params["after"] = after
366
+ data = self._get(SUBSCRIPTIONS_URL, params=params)
367
+ children = data.get("data", {}).get("children", [])
368
+ if not children:
369
+ break
370
+ for child in children:
371
+ name = child.get("data", {}).get("display_name", "")
372
+ if name:
373
+ names.append(name)
374
+ if len(names) >= max_subs:
375
+ break
376
+ after = data.get("data", {}).get("after")
377
+ if not after:
378
+ break
379
+ return names
380
+
381
+ def get_subs_only_feed(
382
+ self,
383
+ limit_per_sub: int = DEFAULT_LIMIT,
384
+ max_subs: int = 20,
385
+ on_progress: Any = None,
386
+ ) -> dict:
387
+ """Aggregate newest posts from subscribed subreddits.
388
+
389
+ Returns a synthetic listing dict compatible with parse_listing().
390
+ """
391
+ subs = self.get_my_subscriptions(max_subs=max_subs)
392
+ if not subs:
393
+ return {"data": {"children": [], "after": None}}
394
+
395
+ all_posts: list[dict] = []
396
+ seen_ids: set[str] = set()
397
+
398
+ for i, sub_name in enumerate(subs):
399
+ if on_progress:
400
+ on_progress(i + 1, len(subs), sub_name)
401
+ try:
402
+ data = self.get_subreddit(sub_name, sort="new", limit=limit_per_sub)
403
+ children = data.get("data", {}).get("children", [])
404
+ for child in children:
405
+ post = child.get("data", child)
406
+ pid = post.get("id", "")
407
+ if pid and pid not in seen_ids:
408
+ seen_ids.add(pid)
409
+ all_posts.append(child if "data" in child else {"data": child})
410
+ except RedditApiError as exc:
411
+ logger.warning("Skipping r/%s: %s", sub_name, exc)
412
+
413
+ # Sort by created_utc descending
414
+ all_posts.sort(
415
+ key=lambda c: c.get("data", {}).get("created_utc", 0),
416
+ reverse=True,
417
+ )
418
+
419
+ return {"data": {"children": all_posts, "after": None}}
@@ -159,24 +159,46 @@ def _resolve_current_username(client: RedditClient) -> str:
159
159
 
160
160
 
161
161
  @click.command()
162
+ @click.option("--subs-only", is_flag=True, help="Show only posts from subscribed subreddits (sorted by time)")
163
+ @click.option("--max-subs", default=20, type=int, help="Max subscriptions to fetch (default: 20)")
162
164
  @click.option("-n", "--limit", default=25, type=int, help="Number of posts (default: 25)")
163
165
  @click.option("--after", default=None, help="Pagination cursor")
164
166
  @listing_options
165
167
  def feed(
168
+ subs_only: bool, max_subs: int,
166
169
  limit: int, after: str | None,
167
170
  as_json: bool, as_yaml: bool,
168
171
  output_file: str | None, full_text: bool, compact: bool,
169
172
  ) -> None:
170
173
  """Browse your home feed (requires login)"""
171
174
  cred = require_auth()
172
- _handle_listing(
173
- cred,
174
- action=lambda c: c.get_home(limit=limit, after=after),
175
- data_title="🏠 Home Feed",
176
- next_cmd="rdt feed",
177
- as_json=as_json, as_yaml=as_yaml,
178
- output_file=output_file, full_text=full_text, compact=compact,
179
- )
175
+ if subs_only:
176
+ if after:
177
+ console.print("[yellow]⚠ --after is ignored with --subs-only[/yellow]")
178
+
179
+ def _progress(current: int, total: int, name: str) -> None:
180
+ if not (as_json or as_yaml):
181
+ console.print(f" [dim]📡 [{current}/{total}] r/{name}[/dim]")
182
+
183
+ _handle_listing(
184
+ cred,
185
+ action=lambda c: c.get_subs_only_feed(
186
+ limit_per_sub=limit, max_subs=max_subs, on_progress=_progress,
187
+ ),
188
+ data_title="📡 Subscriptions Feed",
189
+ next_cmd="",
190
+ as_json=as_json, as_yaml=as_yaml,
191
+ output_file=output_file, full_text=full_text, compact=compact,
192
+ )
193
+ else:
194
+ _handle_listing(
195
+ cred,
196
+ action=lambda c: c.get_home(limit=limit, after=after),
197
+ data_title="🏠 Home Feed",
198
+ next_cmd="rdt feed",
199
+ as_json=as_json, as_yaml=as_yaml,
200
+ output_file=output_file, full_text=full_text, compact=compact,
201
+ )
180
202
 
181
203
 
182
204
  # ── popular ─────────────────────────────────────────────────────────
@@ -49,6 +49,7 @@ SAVE_URL = "/api/save"
49
49
  UNSAVE_URL = "/api/unsave"
50
50
  SUBSCRIBE_URL = "/api/subscribe"
51
51
  COMMENT_URL = "/api/comment"
52
+ SUBSCRIPTIONS_URL = "/subreddits/mine/subscriber.json"
52
53
 
53
54
  # ── Request Headers (Chrome 133, macOS) ─────────────────────────────
54
55
  HEADERS = {
@@ -672,7 +672,7 @@ class TestMockedBrowse:
672
672
  from rdt_cli.auth import Credential
673
673
 
674
674
  cred = Credential(cookies={"reddit_session": "test"}, username="spez")
675
- with patch("rdt_cli.auth.get_credential", return_value=cred):
675
+ with patch("rdt_cli.commands._common.get_credential", return_value=cred):
676
676
  with patch("rdt_cli.client.RedditClient.get_me", return_value={"name": "spez"}):
677
677
  with patch("rdt_cli.client.RedditClient.get_user_saved", return_value=self._mock_listing()):
678
678
  result = runner.invoke(cli, ["saved", "--json"])
@@ -682,13 +682,70 @@ class TestMockedBrowse:
682
682
  from rdt_cli.auth import Credential
683
683
 
684
684
  cred = Credential(cookies={"reddit_session": "test"}, username="spez")
685
- with patch("rdt_cli.auth.get_credential", return_value=cred):
685
+ with patch("rdt_cli.commands._common.get_credential", return_value=cred):
686
686
  with patch("rdt_cli.client.RedditClient.get_me", return_value={"name": "spez"}):
687
687
  with patch("rdt_cli.client.RedditClient.get_user_upvoted", return_value=self._mock_listing()):
688
688
  result = runner.invoke(cli, ["upvoted", "--json"])
689
689
  assert result.exit_code == 0
690
690
 
691
691
 
692
+ # ── Mocked subs-only feed ──────────────────────────────────────────
693
+
694
+
695
+ class TestSubsOnlyFeed:
696
+ """Test --subs-only flag on feed command."""
697
+
698
+ def _mock_subs_listing(self, names):
699
+ """Build a mock /subreddits/mine/subscriber response."""
700
+ return {
701
+ "data": {
702
+ "children": [{"data": {"display_name": n}} for n in names],
703
+ "after": None,
704
+ }
705
+ }
706
+
707
+ def _mock_sub_posts(self, subreddit, created_utc=1700000000):
708
+ return {
709
+ "data": {
710
+ "children": [
711
+ {"data": {"id": f"{subreddit}_1", "title": f"Post from {subreddit}",
712
+ "subreddit": subreddit, "author": "bob", "score": 10,
713
+ "num_comments": 1, "created_utc": created_utc}},
714
+ ],
715
+ "after": None,
716
+ }
717
+ }
718
+
719
+ def test_feed_subs_only_json(self):
720
+ from rdt_cli.auth import Credential
721
+ cred = Credential(cookies={"reddit_session": "test"})
722
+ with patch("rdt_cli.commands._common.get_credential", return_value=cred):
723
+ with patch("rdt_cli.client.RedditClient.get_my_subscriptions", return_value=["python", "rust"]):
724
+ with patch("rdt_cli.client.RedditClient.get_subreddit") as mock_sub:
725
+ mock_sub.side_effect = [
726
+ self._mock_sub_posts("python", created_utc=1700000200),
727
+ self._mock_sub_posts("rust", created_utc=1700000100),
728
+ ]
729
+ result = runner.invoke(cli, ["feed", "--subs-only", "--json"])
730
+ assert result.exit_code == 0
731
+ data = json.loads(result.output)
732
+ assert data["ok"] is True
733
+
734
+ def test_feed_subs_only_empty_subscriptions(self):
735
+ from rdt_cli.auth import Credential
736
+ cred = Credential(cookies={"reddit_session": "test"})
737
+ with patch("rdt_cli.commands._common.get_credential", return_value=cred):
738
+ with patch("rdt_cli.client.RedditClient.get_my_subscriptions", return_value=[]):
739
+ result = runner.invoke(cli, ["feed", "--subs-only", "--json"])
740
+ assert result.exit_code == 0
741
+
742
+ def test_feed_help_shows_subs_only(self):
743
+ result = runner.invoke(cli, ["feed", "--help"])
744
+ assert result.exit_code == 0
745
+ assert "--subs-only" in result.output
746
+ assert "--max-subs" in result.output
747
+
748
+
692
749
  # ── Mocked search commands ──────────────────────────────────────────
693
750
 
694
751
 
@@ -554,6 +554,20 @@ class TestAuthenticatedViews:
554
554
  if data:
555
555
  assert data["ok"] is True
556
556
 
557
+ def test_feed_subs_only(self):
558
+ if not _authenticated():
559
+ pytest.skip("Authentication required for --subs-only")
560
+ result = _invoke("feed", "--subs-only", "-n", "3", "--max-subs", "3")
561
+ assert result.exit_code == 0
562
+
563
+ def test_feed_subs_only_json(self):
564
+ if not _authenticated():
565
+ pytest.skip("Authentication required for --subs-only")
566
+ result, data = _invoke_json("feed", "--subs-only", "-n", "3", "--max-subs", "3")
567
+ assert result.exit_code == 0
568
+ if data:
569
+ assert data["ok"] is True
570
+
557
571
 
558
572
  # ── Pagination ─────────────────────────────────────────────────────
559
573
 
@@ -382,7 +382,7 @@ wheels = [
382
382
 
383
383
  [[package]]
384
384
  name = "rdt-cli"
385
- version = "0.3.2"
385
+ version = "0.4.0"
386
386
  source = { editable = "." }
387
387
  dependencies = [
388
388
  { name = "browser-cookie3" },
@@ -1,3 +0,0 @@
1
- """Reddit CLI."""
2
-
3
- __version__ = "0.3.2"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes