cli-web-hackernews 0.1.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.
- cli_web_hackernews-0.1.0/PKG-INFO +12 -0
- cli_web_hackernews-0.1.0/cli_web/hackernews/README.md +91 -0
- cli_web_hackernews-0.1.0/cli_web/hackernews/__init__.py +0 -0
- cli_web_hackernews-0.1.0/cli_web/hackernews/__main__.py +6 -0
- cli_web_hackernews-0.1.0/cli_web/hackernews/commands/__init__.py +0 -0
- cli_web_hackernews-0.1.0/cli_web/hackernews/commands/actions.py +105 -0
- cli_web_hackernews-0.1.0/cli_web/hackernews/commands/auth.py +80 -0
- cli_web_hackernews-0.1.0/cli_web/hackernews/commands/search.py +69 -0
- cli_web_hackernews-0.1.0/cli_web/hackernews/commands/stories.py +160 -0
- cli_web_hackernews-0.1.0/cli_web/hackernews/commands/user.py +112 -0
- cli_web_hackernews-0.1.0/cli_web/hackernews/core/__init__.py +0 -0
- cli_web_hackernews-0.1.0/cli_web/hackernews/core/auth.py +290 -0
- cli_web_hackernews-0.1.0/cli_web/hackernews/core/client.py +517 -0
- cli_web_hackernews-0.1.0/cli_web/hackernews/core/exceptions.py +63 -0
- cli_web_hackernews-0.1.0/cli_web/hackernews/core/models.py +144 -0
- cli_web_hackernews-0.1.0/cli_web/hackernews/hackernews_cli.py +171 -0
- cli_web_hackernews-0.1.0/cli_web/hackernews/tests/TEST.md +143 -0
- cli_web_hackernews-0.1.0/cli_web/hackernews/tests/__init__.py +0 -0
- cli_web_hackernews-0.1.0/cli_web/hackernews/tests/test_core.py +365 -0
- cli_web_hackernews-0.1.0/cli_web/hackernews/tests/test_e2e.py +267 -0
- cli_web_hackernews-0.1.0/cli_web/hackernews/utils/__init__.py +0 -0
- cli_web_hackernews-0.1.0/cli_web/hackernews/utils/doctor.py +188 -0
- cli_web_hackernews-0.1.0/cli_web/hackernews/utils/helpers.py +73 -0
- cli_web_hackernews-0.1.0/cli_web/hackernews/utils/mcp_server.py +290 -0
- cli_web_hackernews-0.1.0/cli_web/hackernews/utils/output.py +136 -0
- cli_web_hackernews-0.1.0/cli_web/hackernews/utils/repl_skin.py +486 -0
- cli_web_hackernews-0.1.0/cli_web_hackernews.egg-info/PKG-INFO +12 -0
- cli_web_hackernews-0.1.0/cli_web_hackernews.egg-info/SOURCES.txt +32 -0
- cli_web_hackernews-0.1.0/cli_web_hackernews.egg-info/dependency_links.txt +1 -0
- cli_web_hackernews-0.1.0/cli_web_hackernews.egg-info/entry_points.txt +2 -0
- cli_web_hackernews-0.1.0/cli_web_hackernews.egg-info/requires.txt +4 -0
- cli_web_hackernews-0.1.0/cli_web_hackernews.egg-info/top_level.txt +1 -0
- cli_web_hackernews-0.1.0/setup.cfg +4 -0
- cli_web_hackernews-0.1.0/setup.py +25 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cli-web-hackernews
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI for Hacker News — browse, search, upvote, submit, comment, and more
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: click>=8.0
|
|
7
|
+
Requires-Dist: httpx>=0.24
|
|
8
|
+
Requires-Dist: rich>=13.0
|
|
9
|
+
Requires-Dist: prompt_toolkit>=3.0
|
|
10
|
+
Dynamic: requires-dist
|
|
11
|
+
Dynamic: requires-python
|
|
12
|
+
Dynamic: summary
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# cli-web-hackernews
|
|
2
|
+
|
|
3
|
+
CLI for browsing and interacting with Hacker News — top stories, search, comments, user profiles, plus auth-enabled actions (upvote, submit, comment, favorite).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
cd hackernews/agent-harness
|
|
9
|
+
pip install -e .
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
### Browse (no auth required)
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# Browse stories
|
|
18
|
+
cli-web-hackernews stories top # Front page (top 30)
|
|
19
|
+
cli-web-hackernews stories new -n 10 # Newest 10 stories
|
|
20
|
+
cli-web-hackernews stories best # Best stories (all time)
|
|
21
|
+
cli-web-hackernews stories ask # Ask HN
|
|
22
|
+
cli-web-hackernews stories show # Show HN
|
|
23
|
+
cli-web-hackernews stories jobs # Job listings
|
|
24
|
+
|
|
25
|
+
# View a story with comments
|
|
26
|
+
cli-web-hackernews stories view 47530330
|
|
27
|
+
cli-web-hackernews stories view 47530330 -n 5 --json
|
|
28
|
+
|
|
29
|
+
# Search
|
|
30
|
+
cli-web-hackernews search stories "claude code"
|
|
31
|
+
cli-web-hackernews search comments "react hooks" --sort-date -n 5
|
|
32
|
+
|
|
33
|
+
# User profiles
|
|
34
|
+
cli-web-hackernews user view dang
|
|
35
|
+
cli-web-hackernews user view pg --json
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Auth-Enabled Actions
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Login
|
|
42
|
+
cli-web-hackernews auth login # Username/password prompt
|
|
43
|
+
cli-web-hackernews auth login-browser # Login via browser window
|
|
44
|
+
cli-web-hackernews auth status # Check login status
|
|
45
|
+
cli-web-hackernews auth logout # Remove credentials
|
|
46
|
+
|
|
47
|
+
# Interact (requires login)
|
|
48
|
+
cli-web-hackernews upvote 47530330 # Upvote a story
|
|
49
|
+
cli-web-hackernews submit -t "My Title" -u "https://example.com" # Submit link
|
|
50
|
+
cli-web-hackernews submit -t "Ask HN: Question?" --text "Details" # Ask HN
|
|
51
|
+
cli-web-hackernews comment 47530330 "Great article!" # Comment
|
|
52
|
+
cli-web-hackernews favorite 47530330 # Save to favorites
|
|
53
|
+
cli-web-hackernews hide 47530330 # Hide from feed
|
|
54
|
+
|
|
55
|
+
# View your activity
|
|
56
|
+
cli-web-hackernews user favorites # Your favorites
|
|
57
|
+
cli-web-hackernews user submissions # Your submissions
|
|
58
|
+
cli-web-hackernews user threads # Replies to your comments
|
|
59
|
+
cli-web-hackernews user submissions dang # Someone else's submissions
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### JSON output
|
|
63
|
+
|
|
64
|
+
Every command supports `--json` for structured output:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
cli-web-hackernews stories top -n 5 --json
|
|
68
|
+
cli-web-hackernews upvote 47530330 --json
|
|
69
|
+
cli-web-hackernews --json # Propagates to all commands in REPL mode
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### REPL mode
|
|
73
|
+
|
|
74
|
+
Run without arguments to enter interactive mode:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
cli-web-hackernews
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## API Sources
|
|
81
|
+
|
|
82
|
+
- **Firebase API**: `hacker-news.firebaseio.com/v0/` — stories, items, users (public)
|
|
83
|
+
- **Algolia API**: `hn.algolia.com/api/v1/` — full-text search (public)
|
|
84
|
+
- **HN Web**: `news.ycombinator.com` — auth actions (upvote, submit, comment, favorite, hide)
|
|
85
|
+
|
|
86
|
+
## Testing
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
cd hackernews/agent-harness
|
|
90
|
+
python -m pytest cli_web/hackernews/tests/ -v -s
|
|
91
|
+
```
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Actions command group — upvote, submit, comment, favorite, hide (auth required)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from cli_web.hackernews.core import auth
|
|
7
|
+
from cli_web.hackernews.core.client import HackerNewsClient
|
|
8
|
+
from cli_web.hackernews.utils.helpers import handle_errors, print_json, resolve_json_mode
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _auth_client() -> HackerNewsClient:
|
|
12
|
+
"""Create an authenticated client."""
|
|
13
|
+
cookie = auth.get_user_cookie()
|
|
14
|
+
return HackerNewsClient(user_cookie=cookie)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@click.command("upvote")
|
|
18
|
+
@click.argument("item_id", type=int)
|
|
19
|
+
@click.option("--json", "json_mode", is_flag=True, help="Output as JSON.")
|
|
20
|
+
@click.pass_context
|
|
21
|
+
def upvote_cmd(ctx, item_id, json_mode):
|
|
22
|
+
"""Upvote a story or comment by ID. (Requires auth)"""
|
|
23
|
+
json_mode = resolve_json_mode(json_mode)
|
|
24
|
+
with handle_errors(json_mode=json_mode):
|
|
25
|
+
client = _auth_client()
|
|
26
|
+
result = client.upvote(item_id)
|
|
27
|
+
if json_mode:
|
|
28
|
+
print_json(result)
|
|
29
|
+
else:
|
|
30
|
+
click.echo(f"Upvoted item {item_id}")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@click.command("submit")
|
|
34
|
+
@click.option("--title", "-t", required=True, help="Story title.")
|
|
35
|
+
@click.option("--url", "-u", default=None, help="URL to submit (leave blank for Ask HN).")
|
|
36
|
+
@click.option("--text", default=None, help="Text body (for Ask HN or to add context).")
|
|
37
|
+
@click.option("--json", "json_mode", is_flag=True, help="Output as JSON.")
|
|
38
|
+
@click.pass_context
|
|
39
|
+
def submit_cmd(ctx, title, url, text, json_mode):
|
|
40
|
+
"""Submit a new story to Hacker News. (Requires auth)
|
|
41
|
+
|
|
42
|
+
Use --url for link submissions, or leave blank for Ask HN (text only).
|
|
43
|
+
"""
|
|
44
|
+
json_mode = resolve_json_mode(json_mode)
|
|
45
|
+
with handle_errors(json_mode=json_mode):
|
|
46
|
+
client = _auth_client()
|
|
47
|
+
result = client.submit_story(title=title, url=url, text=text)
|
|
48
|
+
if json_mode:
|
|
49
|
+
print_json(result)
|
|
50
|
+
else:
|
|
51
|
+
kind = "link" if url else "Ask HN"
|
|
52
|
+
click.echo(f"Submitted {kind}: {title}")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@click.command("comment")
|
|
56
|
+
@click.argument("parent_id", type=int)
|
|
57
|
+
@click.argument("text")
|
|
58
|
+
@click.option("--json", "json_mode", is_flag=True, help="Output as JSON.")
|
|
59
|
+
@click.pass_context
|
|
60
|
+
def comment_cmd(ctx, parent_id, text, json_mode):
|
|
61
|
+
"""Post a comment on a story or reply to a comment. (Requires auth)
|
|
62
|
+
|
|
63
|
+
PARENT_ID is the story or comment ID to reply to.
|
|
64
|
+
TEXT is the comment body.
|
|
65
|
+
"""
|
|
66
|
+
json_mode = resolve_json_mode(json_mode)
|
|
67
|
+
with handle_errors(json_mode=json_mode):
|
|
68
|
+
client = _auth_client()
|
|
69
|
+
result = client.post_comment(parent_id=parent_id, text=text)
|
|
70
|
+
if json_mode:
|
|
71
|
+
print_json(result)
|
|
72
|
+
else:
|
|
73
|
+
click.echo(f"Comment posted on item {parent_id}")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@click.command("favorite")
|
|
77
|
+
@click.argument("item_id", type=int)
|
|
78
|
+
@click.option("--json", "json_mode", is_flag=True, help="Output as JSON.")
|
|
79
|
+
@click.pass_context
|
|
80
|
+
def favorite_cmd(ctx, item_id, json_mode):
|
|
81
|
+
"""Favorite (save) a story. (Requires auth)"""
|
|
82
|
+
json_mode = resolve_json_mode(json_mode)
|
|
83
|
+
with handle_errors(json_mode=json_mode):
|
|
84
|
+
client = _auth_client()
|
|
85
|
+
result = client.favorite(item_id)
|
|
86
|
+
if json_mode:
|
|
87
|
+
print_json(result)
|
|
88
|
+
else:
|
|
89
|
+
click.echo(f"Favorited item {item_id}")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@click.command("hide")
|
|
93
|
+
@click.argument("item_id", type=int)
|
|
94
|
+
@click.option("--json", "json_mode", is_flag=True, help="Output as JSON.")
|
|
95
|
+
@click.pass_context
|
|
96
|
+
def hide_cmd(ctx, item_id, json_mode):
|
|
97
|
+
"""Hide a story from your feed. (Requires auth)"""
|
|
98
|
+
json_mode = resolve_json_mode(json_mode)
|
|
99
|
+
with handle_errors(json_mode=json_mode):
|
|
100
|
+
client = _auth_client()
|
|
101
|
+
result = client.hide(item_id)
|
|
102
|
+
if json_mode:
|
|
103
|
+
print_json(result)
|
|
104
|
+
else:
|
|
105
|
+
click.echo(f"Hidden item {item_id}")
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Auth command group — login, status, logout."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from cli_web.hackernews.core import auth
|
|
7
|
+
from cli_web.hackernews.utils.helpers import handle_errors, print_json, resolve_json_mode
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@click.group("auth")
|
|
11
|
+
def auth_group():
|
|
12
|
+
"""Manage Hacker News authentication."""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@auth_group.command("login")
|
|
16
|
+
@click.option("--username", "-u", prompt="HN username", help="Your Hacker News username.")
|
|
17
|
+
@click.option("--password", "-p", prompt=True, hide_input=True, help="Your Hacker News password.")
|
|
18
|
+
@click.option("--json", "json_mode", is_flag=True, help="Output as JSON.")
|
|
19
|
+
@click.pass_context
|
|
20
|
+
def auth_login(ctx, username, password, json_mode):
|
|
21
|
+
"""Login to Hacker News with username and password."""
|
|
22
|
+
json_mode = resolve_json_mode(json_mode)
|
|
23
|
+
with handle_errors(json_mode=json_mode):
|
|
24
|
+
result = auth.login_with_password(username, password)
|
|
25
|
+
if json_mode:
|
|
26
|
+
print_json({"success": True, "username": result["username"]})
|
|
27
|
+
else:
|
|
28
|
+
click.echo(f"Logged in as {result['username']}")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@auth_group.command("login-browser")
|
|
32
|
+
@click.option("--json", "json_mode", is_flag=True, help="Output as JSON.")
|
|
33
|
+
@click.pass_context
|
|
34
|
+
def auth_login_browser(ctx, json_mode):
|
|
35
|
+
"""Login to Hacker News via browser (opens a browser window)."""
|
|
36
|
+
json_mode = resolve_json_mode(json_mode)
|
|
37
|
+
with handle_errors(json_mode=json_mode):
|
|
38
|
+
result = auth.login_browser()
|
|
39
|
+
if json_mode:
|
|
40
|
+
print_json({"success": True, "username": result["username"]})
|
|
41
|
+
else:
|
|
42
|
+
click.echo(f"Logged in as {result['username']}")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@auth_group.command("status")
|
|
46
|
+
@click.option("--json", "json_mode", is_flag=True, help="Output as JSON.")
|
|
47
|
+
@click.pass_context
|
|
48
|
+
def auth_status(ctx, json_mode):
|
|
49
|
+
"""Check authentication status."""
|
|
50
|
+
json_mode = resolve_json_mode(json_mode)
|
|
51
|
+
with handle_errors(json_mode=json_mode):
|
|
52
|
+
if not auth.is_logged_in():
|
|
53
|
+
if json_mode:
|
|
54
|
+
print_json({"logged_in": False})
|
|
55
|
+
else:
|
|
56
|
+
click.echo("Not logged in. Run: cli-web-hackernews auth login")
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
result = auth.validate_auth()
|
|
60
|
+
if json_mode:
|
|
61
|
+
print_json(
|
|
62
|
+
{"logged_in": True, "username": result["username"], "valid": result["valid"]}
|
|
63
|
+
)
|
|
64
|
+
else:
|
|
65
|
+
click.echo(f"Logged in as: {result['username']}")
|
|
66
|
+
click.echo(f"Cookie valid: {'yes' if result['valid'] else 'no'}")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@auth_group.command("logout")
|
|
70
|
+
@click.option("--json", "json_mode", is_flag=True, help="Output as JSON.")
|
|
71
|
+
@click.pass_context
|
|
72
|
+
def auth_logout(ctx, json_mode):
|
|
73
|
+
"""Remove stored authentication credentials."""
|
|
74
|
+
json_mode = resolve_json_mode(json_mode)
|
|
75
|
+
with handle_errors(json_mode=json_mode):
|
|
76
|
+
auth.logout()
|
|
77
|
+
if json_mode:
|
|
78
|
+
print_json({"success": True, "message": "Logged out"})
|
|
79
|
+
else:
|
|
80
|
+
click.echo("Logged out successfully.")
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Search command — search HN via Algolia API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from cli_web.hackernews.core.client import HackerNewsClient
|
|
7
|
+
from cli_web.hackernews.utils.helpers import handle_errors, resolve_json_mode
|
|
8
|
+
from cli_web.hackernews.utils.output import print_json, print_search_results_table
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.group("search")
|
|
12
|
+
def search_group():
|
|
13
|
+
"""Search Hacker News stories and comments."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@search_group.command("stories")
|
|
17
|
+
@click.argument("query")
|
|
18
|
+
@click.option("-n", "--limit", default=20, show_default=True, help="Number of results.")
|
|
19
|
+
@click.option("--page", default=0, help="Page number (0-indexed).")
|
|
20
|
+
@click.option("--sort-date", is_flag=True, help="Sort by date instead of relevance.")
|
|
21
|
+
@click.option("--json", "json_mode", is_flag=True, help="Output as JSON.")
|
|
22
|
+
@click.pass_context
|
|
23
|
+
def search_stories(ctx, query, limit, page, sort_date, json_mode):
|
|
24
|
+
"""Search for stories by keyword."""
|
|
25
|
+
json_mode = resolve_json_mode(json_mode)
|
|
26
|
+
with handle_errors(json_mode=json_mode):
|
|
27
|
+
client = HackerNewsClient()
|
|
28
|
+
results = client.search(
|
|
29
|
+
query=query,
|
|
30
|
+
tags="story",
|
|
31
|
+
sort_by_date=sort_date,
|
|
32
|
+
hits_per_page=limit,
|
|
33
|
+
page=page,
|
|
34
|
+
)
|
|
35
|
+
if json_mode:
|
|
36
|
+
print_json([r.to_dict() for r in results])
|
|
37
|
+
else:
|
|
38
|
+
sort_label = "by date" if sort_date else "by relevance"
|
|
39
|
+
click.echo(f"\nSearch: '{query}' ({sort_label})\n")
|
|
40
|
+
print_search_results_table(results)
|
|
41
|
+
click.echo(f"\n{len(results)} results\n")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@search_group.command("comments")
|
|
45
|
+
@click.argument("query")
|
|
46
|
+
@click.option("-n", "--limit", default=20, show_default=True, help="Number of results.")
|
|
47
|
+
@click.option("--page", default=0, help="Page number (0-indexed).")
|
|
48
|
+
@click.option("--sort-date", is_flag=True, help="Sort by date instead of relevance.")
|
|
49
|
+
@click.option("--json", "json_mode", is_flag=True, help="Output as JSON.")
|
|
50
|
+
@click.pass_context
|
|
51
|
+
def search_comments(ctx, query, limit, page, sort_date, json_mode):
|
|
52
|
+
"""Search for comments by keyword."""
|
|
53
|
+
json_mode = resolve_json_mode(json_mode)
|
|
54
|
+
with handle_errors(json_mode=json_mode):
|
|
55
|
+
client = HackerNewsClient()
|
|
56
|
+
results = client.search(
|
|
57
|
+
query=query,
|
|
58
|
+
tags="comment",
|
|
59
|
+
sort_by_date=sort_date,
|
|
60
|
+
hits_per_page=limit,
|
|
61
|
+
page=page,
|
|
62
|
+
)
|
|
63
|
+
if json_mode:
|
|
64
|
+
print_json([r.to_dict() for r in results])
|
|
65
|
+
else:
|
|
66
|
+
sort_label = "by date" if sort_date else "by relevance"
|
|
67
|
+
click.echo(f"\nSearch comments: '{query}' ({sort_label})\n")
|
|
68
|
+
print_search_results_table(results)
|
|
69
|
+
click.echo(f"\n{len(results)} results\n")
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Stories command group — browse HN feeds (top, new, best, ask, show, job)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from cli_web.hackernews.core.client import HackerNewsClient
|
|
7
|
+
from cli_web.hackernews.utils.helpers import handle_errors, resolve_json_mode
|
|
8
|
+
from cli_web.hackernews.utils.output import (
|
|
9
|
+
print_comments_list,
|
|
10
|
+
print_json,
|
|
11
|
+
print_stories_table,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@click.group("stories")
|
|
16
|
+
def stories_group():
|
|
17
|
+
"""Browse Hacker News stories."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@stories_group.command("top")
|
|
21
|
+
@click.option("-n", "--limit", default=30, show_default=True, help="Number of stories to show.")
|
|
22
|
+
@click.option("--json", "json_mode", is_flag=True, help="Output as JSON.")
|
|
23
|
+
@click.pass_context
|
|
24
|
+
def stories_top(ctx, limit, json_mode):
|
|
25
|
+
"""Show top stories from the front page."""
|
|
26
|
+
json_mode = resolve_json_mode(json_mode)
|
|
27
|
+
with handle_errors(json_mode=json_mode):
|
|
28
|
+
client = HackerNewsClient()
|
|
29
|
+
stories = client.get_stories("top", limit=limit)
|
|
30
|
+
if json_mode:
|
|
31
|
+
print_json([s.to_dict() for s in stories])
|
|
32
|
+
else:
|
|
33
|
+
click.echo("\nTop Stories\n")
|
|
34
|
+
print_stories_table(stories)
|
|
35
|
+
click.echo(f"\n{len(stories)} stories\n")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@stories_group.command("new")
|
|
39
|
+
@click.option("-n", "--limit", default=30, show_default=True, help="Number of stories to show.")
|
|
40
|
+
@click.option("--json", "json_mode", is_flag=True, help="Output as JSON.")
|
|
41
|
+
@click.pass_context
|
|
42
|
+
def stories_new(ctx, limit, json_mode):
|
|
43
|
+
"""Show newest stories."""
|
|
44
|
+
json_mode = resolve_json_mode(json_mode)
|
|
45
|
+
with handle_errors(json_mode=json_mode):
|
|
46
|
+
client = HackerNewsClient()
|
|
47
|
+
stories = client.get_stories("new", limit=limit)
|
|
48
|
+
if json_mode:
|
|
49
|
+
print_json([s.to_dict() for s in stories])
|
|
50
|
+
else:
|
|
51
|
+
click.echo("\nNew Stories\n")
|
|
52
|
+
print_stories_table(stories)
|
|
53
|
+
click.echo(f"\n{len(stories)} stories\n")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@stories_group.command("best")
|
|
57
|
+
@click.option("-n", "--limit", default=30, show_default=True, help="Number of stories to show.")
|
|
58
|
+
@click.option("--json", "json_mode", is_flag=True, help="Output as JSON.")
|
|
59
|
+
@click.pass_context
|
|
60
|
+
def stories_best(ctx, limit, json_mode):
|
|
61
|
+
"""Show best stories (all time)."""
|
|
62
|
+
json_mode = resolve_json_mode(json_mode)
|
|
63
|
+
with handle_errors(json_mode=json_mode):
|
|
64
|
+
client = HackerNewsClient()
|
|
65
|
+
stories = client.get_stories("best", limit=limit)
|
|
66
|
+
if json_mode:
|
|
67
|
+
print_json([s.to_dict() for s in stories])
|
|
68
|
+
else:
|
|
69
|
+
click.echo("\nBest Stories\n")
|
|
70
|
+
print_stories_table(stories)
|
|
71
|
+
click.echo(f"\n{len(stories)} stories\n")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@stories_group.command("ask")
|
|
75
|
+
@click.option("-n", "--limit", default=30, show_default=True, help="Number of stories to show.")
|
|
76
|
+
@click.option("--json", "json_mode", is_flag=True, help="Output as JSON.")
|
|
77
|
+
@click.pass_context
|
|
78
|
+
def stories_ask(ctx, limit, json_mode):
|
|
79
|
+
"""Show Ask HN stories."""
|
|
80
|
+
json_mode = resolve_json_mode(json_mode)
|
|
81
|
+
with handle_errors(json_mode=json_mode):
|
|
82
|
+
client = HackerNewsClient()
|
|
83
|
+
stories = client.get_stories("ask", limit=limit)
|
|
84
|
+
if json_mode:
|
|
85
|
+
print_json([s.to_dict() for s in stories])
|
|
86
|
+
else:
|
|
87
|
+
click.echo("\nAsk HN\n")
|
|
88
|
+
print_stories_table(stories)
|
|
89
|
+
click.echo(f"\n{len(stories)} stories\n")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@stories_group.command("show")
|
|
93
|
+
@click.option("-n", "--limit", default=30, show_default=True, help="Number of stories to show.")
|
|
94
|
+
@click.option("--json", "json_mode", is_flag=True, help="Output as JSON.")
|
|
95
|
+
@click.pass_context
|
|
96
|
+
def stories_show(ctx, limit, json_mode):
|
|
97
|
+
"""Show Show HN stories."""
|
|
98
|
+
json_mode = resolve_json_mode(json_mode)
|
|
99
|
+
with handle_errors(json_mode=json_mode):
|
|
100
|
+
client = HackerNewsClient()
|
|
101
|
+
stories = client.get_stories("show", limit=limit)
|
|
102
|
+
if json_mode:
|
|
103
|
+
print_json([s.to_dict() for s in stories])
|
|
104
|
+
else:
|
|
105
|
+
click.echo("\nShow HN\n")
|
|
106
|
+
print_stories_table(stories)
|
|
107
|
+
click.echo(f"\n{len(stories)} stories\n")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@stories_group.command("jobs")
|
|
111
|
+
@click.option("-n", "--limit", default=30, show_default=True, help="Number of stories to show.")
|
|
112
|
+
@click.option("--json", "json_mode", is_flag=True, help="Output as JSON.")
|
|
113
|
+
@click.pass_context
|
|
114
|
+
def stories_jobs(ctx, limit, json_mode):
|
|
115
|
+
"""Show job stories."""
|
|
116
|
+
json_mode = resolve_json_mode(json_mode)
|
|
117
|
+
with handle_errors(json_mode=json_mode):
|
|
118
|
+
client = HackerNewsClient()
|
|
119
|
+
stories = client.get_stories("job", limit=limit)
|
|
120
|
+
if json_mode:
|
|
121
|
+
print_json([s.to_dict() for s in stories])
|
|
122
|
+
else:
|
|
123
|
+
click.echo("\nJobs\n")
|
|
124
|
+
print_stories_table(stories)
|
|
125
|
+
click.echo(f"\n{len(stories)} stories\n")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@stories_group.command("view")
|
|
129
|
+
@click.argument("story_id", type=int)
|
|
130
|
+
@click.option("--comments/--no-comments", default=True, help="Show comments.")
|
|
131
|
+
@click.option("-n", "--limit", default=10, show_default=True, help="Number of comments to show.")
|
|
132
|
+
@click.option("--json", "json_mode", is_flag=True, help="Output as JSON.")
|
|
133
|
+
@click.pass_context
|
|
134
|
+
def stories_view(ctx, story_id, comments, limit, json_mode):
|
|
135
|
+
"""View a story and its comments by ID."""
|
|
136
|
+
json_mode = resolve_json_mode(json_mode)
|
|
137
|
+
with handle_errors(json_mode=json_mode):
|
|
138
|
+
client = HackerNewsClient()
|
|
139
|
+
story = client.get_story(story_id)
|
|
140
|
+
|
|
141
|
+
if json_mode:
|
|
142
|
+
data = story.to_dict()
|
|
143
|
+
if comments:
|
|
144
|
+
cmts = client.get_comments(story_id, limit=limit)
|
|
145
|
+
data["comments"] = [c.to_dict() for c in cmts]
|
|
146
|
+
print_json(data)
|
|
147
|
+
else:
|
|
148
|
+
click.echo(f"\n {story.title}")
|
|
149
|
+
if story.url:
|
|
150
|
+
click.echo(f" {story.url}")
|
|
151
|
+
click.echo(
|
|
152
|
+
f" {story.score} points by {story.by} | {story.age} | {story.descendants} comments"
|
|
153
|
+
)
|
|
154
|
+
click.echo()
|
|
155
|
+
|
|
156
|
+
if comments:
|
|
157
|
+
cmts = client.get_comments(story_id, limit=limit)
|
|
158
|
+
if cmts:
|
|
159
|
+
click.echo(" Comments:\n")
|
|
160
|
+
print_comments_list(cmts)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""User command — view HN user profiles, favorites, and submissions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from cli_web.hackernews.core import auth
|
|
7
|
+
from cli_web.hackernews.core.client import HackerNewsClient
|
|
8
|
+
from cli_web.hackernews.utils.helpers import handle_errors, resolve_json_mode
|
|
9
|
+
from cli_web.hackernews.utils.output import (
|
|
10
|
+
print_comments_list,
|
|
11
|
+
print_json,
|
|
12
|
+
print_stories_table,
|
|
13
|
+
print_user_profile,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@click.group("user")
|
|
18
|
+
def user_group():
|
|
19
|
+
"""View Hacker News user profiles."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@user_group.command("view")
|
|
23
|
+
@click.argument("username")
|
|
24
|
+
@click.option("--json", "json_mode", is_flag=True, help="Output as JSON.")
|
|
25
|
+
@click.pass_context
|
|
26
|
+
def user_view(ctx, username, json_mode):
|
|
27
|
+
"""View a user's profile by username."""
|
|
28
|
+
json_mode = resolve_json_mode(json_mode)
|
|
29
|
+
with handle_errors(json_mode=json_mode):
|
|
30
|
+
client = HackerNewsClient()
|
|
31
|
+
user = client.get_user(username)
|
|
32
|
+
if json_mode:
|
|
33
|
+
print_json(user.to_dict())
|
|
34
|
+
else:
|
|
35
|
+
click.echo(f"\nUser Profile: {username}\n")
|
|
36
|
+
print_user_profile(user)
|
|
37
|
+
click.echo()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@user_group.command("favorites")
|
|
41
|
+
@click.argument("username", required=False)
|
|
42
|
+
@click.option("-n", "--limit", default=30, show_default=True, help="Number of items to show.")
|
|
43
|
+
@click.option("--json", "json_mode", is_flag=True, help="Output as JSON.")
|
|
44
|
+
@click.pass_context
|
|
45
|
+
def user_favorites(ctx, username, limit, json_mode):
|
|
46
|
+
"""View a user's favorite stories. (Requires auth)
|
|
47
|
+
|
|
48
|
+
If USERNAME is omitted, shows your own favorites.
|
|
49
|
+
"""
|
|
50
|
+
json_mode = resolve_json_mode(json_mode)
|
|
51
|
+
with handle_errors(json_mode=json_mode):
|
|
52
|
+
if not username:
|
|
53
|
+
username = auth.get_username()
|
|
54
|
+
cookie = auth.get_user_cookie()
|
|
55
|
+
client = HackerNewsClient(user_cookie=cookie)
|
|
56
|
+
stories = client.get_favorites(username, limit=limit)
|
|
57
|
+
if json_mode:
|
|
58
|
+
print_json([s.to_dict() for s in stories])
|
|
59
|
+
else:
|
|
60
|
+
click.echo(f"\n{username}'s Favorites\n")
|
|
61
|
+
print_stories_table(stories)
|
|
62
|
+
click.echo(f"\n{len(stories)} stories\n")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@user_group.command("submissions")
|
|
66
|
+
@click.argument("username", required=False)
|
|
67
|
+
@click.option("-n", "--limit", default=30, show_default=True, help="Number of items to show.")
|
|
68
|
+
@click.option("--json", "json_mode", is_flag=True, help="Output as JSON.")
|
|
69
|
+
@click.pass_context
|
|
70
|
+
def user_submissions(ctx, username, limit, json_mode):
|
|
71
|
+
"""View a user's submitted stories. (Requires auth)
|
|
72
|
+
|
|
73
|
+
If USERNAME is omitted, shows your own submissions.
|
|
74
|
+
"""
|
|
75
|
+
json_mode = resolve_json_mode(json_mode)
|
|
76
|
+
with handle_errors(json_mode=json_mode):
|
|
77
|
+
if not username:
|
|
78
|
+
username = auth.get_username()
|
|
79
|
+
cookie = auth.get_user_cookie()
|
|
80
|
+
client = HackerNewsClient(user_cookie=cookie)
|
|
81
|
+
stories = client.get_submissions(username, limit=limit)
|
|
82
|
+
if json_mode:
|
|
83
|
+
print_json([s.to_dict() for s in stories])
|
|
84
|
+
else:
|
|
85
|
+
click.echo(f"\n{username}'s Submissions\n")
|
|
86
|
+
print_stories_table(stories)
|
|
87
|
+
click.echo(f"\n{len(stories)} stories\n")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@user_group.command("threads")
|
|
91
|
+
@click.argument("username", required=False)
|
|
92
|
+
@click.option("-n", "--limit", default=20, show_default=True, help="Number of comments to show.")
|
|
93
|
+
@click.option("--json", "json_mode", is_flag=True, help="Output as JSON.")
|
|
94
|
+
@click.pass_context
|
|
95
|
+
def user_threads(ctx, username, limit, json_mode):
|
|
96
|
+
"""View comment replies to a user (your threads). (Requires auth)
|
|
97
|
+
|
|
98
|
+
If USERNAME is omitted, shows replies to your own comments.
|
|
99
|
+
"""
|
|
100
|
+
json_mode = resolve_json_mode(json_mode)
|
|
101
|
+
with handle_errors(json_mode=json_mode):
|
|
102
|
+
if not username:
|
|
103
|
+
username = auth.get_username()
|
|
104
|
+
cookie = auth.get_user_cookie()
|
|
105
|
+
client = HackerNewsClient(user_cookie=cookie)
|
|
106
|
+
comments = client.get_threads(username, limit=limit)
|
|
107
|
+
if json_mode:
|
|
108
|
+
print_json([c.to_dict() for c in comments])
|
|
109
|
+
else:
|
|
110
|
+
click.echo(f"\n{username}'s Threads (replies)\n")
|
|
111
|
+
print_comments_list(comments)
|
|
112
|
+
click.echo(f"\n{len(comments)} comments\n")
|
|
File without changes
|