scholarinboxcli 0.1.0__py3-none-any.whl → 0.1.2__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.
- scholarinboxcli/__init__.py +1 -1
- scholarinboxcli/api/client.py +96 -67
- scholarinboxcli/api/endpoints.py +54 -0
- scholarinboxcli/cli.py +11 -505
- scholarinboxcli/commands/__init__.py +1 -0
- scholarinboxcli/commands/auth.py +39 -0
- scholarinboxcli/commands/bookmarks.py +49 -0
- scholarinboxcli/commands/collections.py +135 -0
- scholarinboxcli/commands/common.py +59 -0
- scholarinboxcli/commands/conferences.py +35 -0
- scholarinboxcli/commands/papers.py +88 -0
- scholarinboxcli/formatters/domain_tables.py +122 -0
- scholarinboxcli/formatters/table.py +93 -25
- scholarinboxcli/services/__init__.py +1 -0
- scholarinboxcli/services/collections.py +130 -0
- scholarinboxcli/services/paper_sort.py +54 -0
- {scholarinboxcli-0.1.0.dist-info → scholarinboxcli-0.1.2.dist-info}/METADATA +13 -46
- scholarinboxcli-0.1.2.dist-info/RECORD +23 -0
- scholarinboxcli-0.1.2.dist-info/licenses/LICENSE +21 -0
- scholarinboxcli-0.1.0.dist-info/RECORD +0 -10
- {scholarinboxcli-0.1.0.dist-info → scholarinboxcli-0.1.2.dist-info}/WHEEL +0 -0
- {scholarinboxcli-0.1.0.dist-info → scholarinboxcli-0.1.2.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Collection command group."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from scholarinboxcli.commands.common import print_output, with_client
|
|
10
|
+
from scholarinboxcli.formatters.domain_tables import format_collection_list, format_collection_papers
|
|
11
|
+
from scholarinboxcli.services.collections import resolve_collection_id
|
|
12
|
+
from scholarinboxcli.services.paper_sort import sort_paper_response
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(help="Collection commands", no_args_is_help=True)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@app.command("list")
|
|
19
|
+
def collection_list(
|
|
20
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
21
|
+
expanded: bool = typer.Option(False, "--expanded", help="Use expanded collection metadata"),
|
|
22
|
+
no_retry: bool = typer.Option(False, "--no-retry", help="Disable retry on rate limits"),
|
|
23
|
+
):
|
|
24
|
+
def action(client):
|
|
25
|
+
data = client.collections_expanded() if expanded else client.collections_list()
|
|
26
|
+
print_output(data, json_output, title="Collections", table_formatter=format_collection_list)
|
|
27
|
+
|
|
28
|
+
with_client(no_retry, action)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@app.command("create")
|
|
32
|
+
def collection_create(
|
|
33
|
+
name: str = typer.Argument(..., help="Collection name"),
|
|
34
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
35
|
+
no_retry: bool = typer.Option(False, "--no-retry", help="Disable retry on rate limits"),
|
|
36
|
+
):
|
|
37
|
+
def action(client):
|
|
38
|
+
data = client.collection_create(name)
|
|
39
|
+
print_output(data, json_output, title="Collection created")
|
|
40
|
+
|
|
41
|
+
with_client(no_retry, action)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@app.command("rename")
|
|
45
|
+
def collection_rename(
|
|
46
|
+
collection_id: str = typer.Argument(..., help="Collection ID or name"),
|
|
47
|
+
new_name: str = typer.Argument(..., help="New collection name"),
|
|
48
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
49
|
+
no_retry: bool = typer.Option(False, "--no-retry", help="Disable retry on rate limits"),
|
|
50
|
+
):
|
|
51
|
+
def action(client):
|
|
52
|
+
cid = resolve_collection_id(client, collection_id)
|
|
53
|
+
data = client.collection_rename(cid, new_name)
|
|
54
|
+
print_output(data, json_output, title="Collection renamed")
|
|
55
|
+
|
|
56
|
+
with_client(no_retry, action)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@app.command("delete")
|
|
60
|
+
def collection_delete(
|
|
61
|
+
collection_id: str = typer.Argument(..., help="Collection ID or name"),
|
|
62
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
63
|
+
no_retry: bool = typer.Option(False, "--no-retry", help="Disable retry on rate limits"),
|
|
64
|
+
):
|
|
65
|
+
def action(client):
|
|
66
|
+
cid = resolve_collection_id(client, collection_id)
|
|
67
|
+
data = client.collection_delete(cid)
|
|
68
|
+
print_output(data, json_output, title="Collection deleted")
|
|
69
|
+
|
|
70
|
+
with_client(no_retry, action)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@app.command("add")
|
|
74
|
+
def collection_add(
|
|
75
|
+
collection_id: str = typer.Argument(..., help="Collection ID or name"),
|
|
76
|
+
paper_id: str = typer.Argument(..., help="Paper ID"),
|
|
77
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
78
|
+
no_retry: bool = typer.Option(False, "--no-retry", help="Disable retry on rate limits"),
|
|
79
|
+
):
|
|
80
|
+
def action(client):
|
|
81
|
+
cid = resolve_collection_id(client, collection_id)
|
|
82
|
+
data = client.collection_add_paper(cid, paper_id)
|
|
83
|
+
print_output(data, json_output, title="Collection add paper")
|
|
84
|
+
|
|
85
|
+
with_client(no_retry, action)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@app.command("remove")
|
|
89
|
+
def collection_remove(
|
|
90
|
+
collection_id: str = typer.Argument(..., help="Collection ID or name"),
|
|
91
|
+
paper_id: str = typer.Argument(..., help="Paper ID"),
|
|
92
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
93
|
+
no_retry: bool = typer.Option(False, "--no-retry", help="Disable retry on rate limits"),
|
|
94
|
+
):
|
|
95
|
+
def action(client):
|
|
96
|
+
cid = resolve_collection_id(client, collection_id)
|
|
97
|
+
data = client.collection_remove_paper(cid, paper_id)
|
|
98
|
+
print_output(data, json_output, title="Collection remove paper")
|
|
99
|
+
|
|
100
|
+
with_client(no_retry, action)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@app.command("papers")
|
|
104
|
+
def collection_papers(
|
|
105
|
+
collection_id: str = typer.Argument(..., help="Collection ID or name"),
|
|
106
|
+
limit: Optional[int] = typer.Option(None, "--limit", "-n", help="Limit results"),
|
|
107
|
+
offset: Optional[int] = typer.Option(None, "--offset", help="Pagination offset"),
|
|
108
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
109
|
+
no_retry: bool = typer.Option(False, "--no-retry", help="Disable retry on rate limits"),
|
|
110
|
+
):
|
|
111
|
+
def action(client):
|
|
112
|
+
cid = resolve_collection_id(client, collection_id)
|
|
113
|
+
data = client.collection_papers(cid, limit=limit, offset=offset)
|
|
114
|
+
print_output(data, json_output, title=f"Collection {cid}", table_formatter=format_collection_papers)
|
|
115
|
+
|
|
116
|
+
with_client(no_retry, action)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@app.command("similar")
|
|
120
|
+
def collection_similar(
|
|
121
|
+
collection_ids: list[str] = typer.Argument(..., help="Collection ID(s) or names"),
|
|
122
|
+
limit: Optional[int] = typer.Option(None, "--limit", "-n", help="Limit results"),
|
|
123
|
+
offset: Optional[int] = typer.Option(None, "--offset", help="Pagination offset"),
|
|
124
|
+
sort_by: Optional[str] = typer.Option(None, "--sort", help="Sort papers by: year, title"),
|
|
125
|
+
asc: bool = typer.Option(False, "--asc", help="Sort ascending (default is descending)"),
|
|
126
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
127
|
+
no_retry: bool = typer.Option(False, "--no-retry", help="Disable retry on rate limits"),
|
|
128
|
+
):
|
|
129
|
+
def action(client):
|
|
130
|
+
resolved = [resolve_collection_id(client, cid) for cid in collection_ids]
|
|
131
|
+
data = client.collections_similar(resolved, limit=limit, offset=offset)
|
|
132
|
+
data = sort_paper_response(data, sort_by, asc)
|
|
133
|
+
print_output(data, json_output, title="Similar Papers")
|
|
134
|
+
|
|
135
|
+
with_client(no_retry, action)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Shared command helpers for output and error handling."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from typing import Any, Callable, TypeVar
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from scholarinboxcli.api.client import ApiError, ScholarInboxClient
|
|
11
|
+
from scholarinboxcli.formatters.json_fmt import format_json
|
|
12
|
+
from scholarinboxcli.formatters.table import format_table
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def print_output(
|
|
16
|
+
data: Any,
|
|
17
|
+
use_json: bool,
|
|
18
|
+
title: str | None = None,
|
|
19
|
+
table_formatter: Callable[[Any, str | None], str] | None = None,
|
|
20
|
+
) -> None:
|
|
21
|
+
if use_json or not sys.stdout.isatty():
|
|
22
|
+
typer.echo(format_json(data))
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
formatter = table_formatter or format_table
|
|
26
|
+
table = formatter(data, title)
|
|
27
|
+
if table == "(no results)":
|
|
28
|
+
typer.echo(table)
|
|
29
|
+
return
|
|
30
|
+
typer.echo(table)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def handle_error(err: ApiError) -> None:
|
|
34
|
+
if not sys.stdout.isatty():
|
|
35
|
+
typer.echo(format_json({"error": err.message, "status_code": err.status_code, "detail": err.detail}))
|
|
36
|
+
else:
|
|
37
|
+
typer.echo(f"Error: {err.message}", err=True)
|
|
38
|
+
if err.status_code:
|
|
39
|
+
typer.echo(f"Status: {err.status_code}", err=True)
|
|
40
|
+
raise typer.Exit(1)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def close_client(client: ScholarInboxClient) -> None:
|
|
44
|
+
client.close()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
T = TypeVar("T")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def with_client(no_retry: bool, action: Callable[[ScholarInboxClient], T]) -> T:
|
|
51
|
+
"""Run action with a managed client and standardized ApiError handling."""
|
|
52
|
+
client = ScholarInboxClient(no_retry=no_retry)
|
|
53
|
+
try:
|
|
54
|
+
return action(client)
|
|
55
|
+
except ApiError as err:
|
|
56
|
+
handle_error(err)
|
|
57
|
+
raise # unreachable, keeps type-checkers happy
|
|
58
|
+
finally:
|
|
59
|
+
close_client(client)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Conference command group."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from scholarinboxcli.commands.common import print_output, with_client
|
|
8
|
+
from scholarinboxcli.formatters.domain_tables import format_conference_explore, format_conference_list
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
app = typer.Typer(help="Conference commands", no_args_is_help=True)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@app.command("list")
|
|
15
|
+
def conference_list(
|
|
16
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
17
|
+
no_retry: bool = typer.Option(False, "--no-retry", help="Disable retry on rate limits"),
|
|
18
|
+
):
|
|
19
|
+
def action(client):
|
|
20
|
+
data = client.conference_list()
|
|
21
|
+
print_output(data, json_output, title="Conferences", table_formatter=format_conference_list)
|
|
22
|
+
|
|
23
|
+
with_client(no_retry, action)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@app.command("explore")
|
|
27
|
+
def conference_explore(
|
|
28
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
29
|
+
no_retry: bool = typer.Option(False, "--no-retry", help="Disable retry on rate limits"),
|
|
30
|
+
):
|
|
31
|
+
def action(client):
|
|
32
|
+
data = client.conference_explorer()
|
|
33
|
+
print_output(data, json_output, title="Conference Explorer", table_formatter=format_conference_explore)
|
|
34
|
+
|
|
35
|
+
with_client(no_retry, action)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Top-level feed/search related commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from scholarinboxcli.commands.common import print_output, with_client
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def register(app: typer.Typer) -> None:
|
|
13
|
+
@app.command("digest")
|
|
14
|
+
def digest(
|
|
15
|
+
date: Optional[str] = typer.Option(None, "--date", help="Digest date (MM-DD-YYYY)"),
|
|
16
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
17
|
+
no_retry: bool = typer.Option(False, "--no-retry", help="Disable retry on rate limits"),
|
|
18
|
+
):
|
|
19
|
+
def action(client):
|
|
20
|
+
data = client.get_digest(date)
|
|
21
|
+
print_output(data, json_output, title="Digest")
|
|
22
|
+
|
|
23
|
+
with_client(no_retry, action)
|
|
24
|
+
|
|
25
|
+
@app.command("trending")
|
|
26
|
+
def trending(
|
|
27
|
+
category: str = typer.Option("ALL", "--category", help="Category filter"),
|
|
28
|
+
days: int = typer.Option(7, "--days", help="Lookback window in days"),
|
|
29
|
+
sort: str = typer.Option("hype", "--sort", help="Sort column"),
|
|
30
|
+
asc: bool = typer.Option(False, "--asc", help="Sort ascending"),
|
|
31
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
32
|
+
no_retry: bool = typer.Option(False, "--no-retry", help="Disable retry on rate limits"),
|
|
33
|
+
):
|
|
34
|
+
def action(client):
|
|
35
|
+
data = client.get_trending(category=category, days=days, sort=sort, asc=asc)
|
|
36
|
+
print_output(data, json_output, title="Trending")
|
|
37
|
+
|
|
38
|
+
with_client(no_retry, action)
|
|
39
|
+
|
|
40
|
+
@app.command("search")
|
|
41
|
+
def search(
|
|
42
|
+
query: str = typer.Argument(..., help="Search query"),
|
|
43
|
+
sort: Optional[str] = typer.Option(None, "--sort", help="Sort option"),
|
|
44
|
+
limit: Optional[int] = typer.Option(None, "--limit", "-n", help="Limit results"),
|
|
45
|
+
offset: Optional[int] = typer.Option(None, "--offset", help="Pagination offset"),
|
|
46
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
47
|
+
no_retry: bool = typer.Option(False, "--no-retry", help="Disable retry on rate limits"),
|
|
48
|
+
):
|
|
49
|
+
def action(client):
|
|
50
|
+
data = client.search(query=query, sort=sort, limit=limit, offset=offset)
|
|
51
|
+
print_output(data, json_output, title="Search")
|
|
52
|
+
|
|
53
|
+
with_client(no_retry, action)
|
|
54
|
+
|
|
55
|
+
@app.command("semantic")
|
|
56
|
+
def semantic_search(
|
|
57
|
+
text: Optional[str] = typer.Argument(None, help="Semantic search text"),
|
|
58
|
+
file: Optional[str] = typer.Option(None, "--file", help="Read query text from file"),
|
|
59
|
+
limit: Optional[int] = typer.Option(None, "--limit", "-n", help="Limit results"),
|
|
60
|
+
offset: Optional[int] = typer.Option(None, "--offset", help="Pagination offset"),
|
|
61
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
62
|
+
no_retry: bool = typer.Option(False, "--no-retry", help="Disable retry on rate limits"),
|
|
63
|
+
):
|
|
64
|
+
if not text and not file:
|
|
65
|
+
typer.echo("Provide text or --file", err=True)
|
|
66
|
+
raise typer.Exit(1)
|
|
67
|
+
if file:
|
|
68
|
+
text = open(file, "r", encoding="utf-8").read()
|
|
69
|
+
|
|
70
|
+
def action(client):
|
|
71
|
+
data = client.semantic_search(text=text or "", limit=limit, offset=offset)
|
|
72
|
+
print_output(data, json_output, title="Semantic Search")
|
|
73
|
+
|
|
74
|
+
with_client(no_retry, action)
|
|
75
|
+
|
|
76
|
+
@app.command("interactions")
|
|
77
|
+
def interactions(
|
|
78
|
+
type_: str = typer.Option("all", "--type", help="Interaction type (all/up/down)"),
|
|
79
|
+
sort: str = typer.Option("ranking_score", "--sort", help="Sort column"),
|
|
80
|
+
asc: bool = typer.Option(False, "--asc", help="Sort ascending"),
|
|
81
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
82
|
+
no_retry: bool = typer.Option(False, "--no-retry", help="Disable retry on rate limits"),
|
|
83
|
+
):
|
|
84
|
+
def action(client):
|
|
85
|
+
data = client.interactions(type_=type_, sort=sort, asc=asc)
|
|
86
|
+
print_output(data, json_output, title="Interactions")
|
|
87
|
+
|
|
88
|
+
with_client(no_retry, action)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Domain-specific table formatters for non-paper responses."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
|
|
10
|
+
from scholarinboxcli.formatters.table import format_table
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _render(table: Table) -> str:
|
|
14
|
+
console = Console()
|
|
15
|
+
with console.capture() as capture:
|
|
16
|
+
console.print(table)
|
|
17
|
+
return capture.get()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def format_auth_status(data: Any, title: str | None = None) -> str:
|
|
21
|
+
if not isinstance(data, dict):
|
|
22
|
+
return format_table(data, title)
|
|
23
|
+
table = Table(title=title)
|
|
24
|
+
table.add_column("Field", overflow="fold")
|
|
25
|
+
table.add_column("Value", overflow="fold")
|
|
26
|
+
for key, value in data.items():
|
|
27
|
+
table.add_row(str(key), str(value))
|
|
28
|
+
return _render(table)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def format_collection_list(data: Any, title: str | None = None) -> str:
|
|
32
|
+
if isinstance(data, list) and data and isinstance(data[0], str):
|
|
33
|
+
table = Table(title=title)
|
|
34
|
+
table.add_column("#", justify="right")
|
|
35
|
+
table.add_column("Name", overflow="fold")
|
|
36
|
+
for i, name in enumerate(data, start=1):
|
|
37
|
+
table.add_row(str(i), str(name))
|
|
38
|
+
return _render(table)
|
|
39
|
+
|
|
40
|
+
if isinstance(data, list) and data and isinstance(data[0], dict):
|
|
41
|
+
table = Table(title=title)
|
|
42
|
+
table.add_column("ID", overflow="fold")
|
|
43
|
+
table.add_column("Name", overflow="fold")
|
|
44
|
+
for item in data:
|
|
45
|
+
cid = item.get("id") or item.get("collection_id") or ""
|
|
46
|
+
name = item.get("name") or item.get("collection_name") or ""
|
|
47
|
+
table.add_row(str(cid), str(name))
|
|
48
|
+
return _render(table)
|
|
49
|
+
|
|
50
|
+
if isinstance(data, dict) and "expanded_collections" in data:
|
|
51
|
+
return format_collection_list(data.get("expanded_collections"), title)
|
|
52
|
+
|
|
53
|
+
return format_table(data, title)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def format_conference_list(data: Any, title: str | None = None) -> str:
|
|
57
|
+
rows = data.get("conferences") if isinstance(data, dict) else None
|
|
58
|
+
if isinstance(rows, list):
|
|
59
|
+
table = Table(title=title)
|
|
60
|
+
table.add_column("ID", justify="right")
|
|
61
|
+
table.add_column("Short")
|
|
62
|
+
table.add_column("Dates")
|
|
63
|
+
table.add_column("URL")
|
|
64
|
+
for row in rows:
|
|
65
|
+
cid = row.get("conference_id", "")
|
|
66
|
+
short = row.get("short_title") or row.get("full_title") or ""
|
|
67
|
+
start = row.get("start_date") or ""
|
|
68
|
+
end = row.get("end_date") or ""
|
|
69
|
+
dates = f"{start} -> {end}" if (start or end) else ""
|
|
70
|
+
url = row.get("conference_url") or ""
|
|
71
|
+
table.add_row(str(cid), str(short), str(dates), str(url))
|
|
72
|
+
return _render(table)
|
|
73
|
+
return format_table(data, title)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def format_conference_explore(data: Any, title: str | None = None) -> str:
|
|
77
|
+
rows = data.get("conf_data_list") if isinstance(data, dict) else None
|
|
78
|
+
if isinstance(rows, list):
|
|
79
|
+
table = Table(title=title)
|
|
80
|
+
table.add_column("Abbrev")
|
|
81
|
+
table.add_column("Conference")
|
|
82
|
+
table.add_column("Relevance", justify="right")
|
|
83
|
+
table.add_column("Years")
|
|
84
|
+
for row in rows:
|
|
85
|
+
abbrev = row.get("abbreviation") or ""
|
|
86
|
+
name = row.get("conference_name") or ""
|
|
87
|
+
rel = row.get("conf_relevance")
|
|
88
|
+
rel_str = f"{rel:.3f}" if isinstance(rel, (float, int)) else ""
|
|
89
|
+
years = row.get("list_of_years") or []
|
|
90
|
+
years_str = ", ".join(str(y) for y in years[:5])
|
|
91
|
+
table.add_row(str(abbrev), str(name), rel_str, years_str)
|
|
92
|
+
return _render(table)
|
|
93
|
+
return format_table(data, title)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _extract_collection_papers(data: Any) -> list[dict[str, Any]]:
|
|
97
|
+
if isinstance(data, list):
|
|
98
|
+
return [item for item in data if isinstance(item, dict)]
|
|
99
|
+
if isinstance(data, dict):
|
|
100
|
+
for key in ("papers", "digest_df", "items", "results", "data"):
|
|
101
|
+
val = data.get(key)
|
|
102
|
+
if isinstance(val, list):
|
|
103
|
+
return [item for item in val if isinstance(item, dict)]
|
|
104
|
+
collections = data.get("collections")
|
|
105
|
+
if isinstance(collections, list):
|
|
106
|
+
papers: list[dict[str, Any]] = []
|
|
107
|
+
for collection in collections:
|
|
108
|
+
if isinstance(collection, dict):
|
|
109
|
+
for key in ("papers", "digest_df"):
|
|
110
|
+
val = collection.get(key)
|
|
111
|
+
if isinstance(val, list):
|
|
112
|
+
papers.extend([item for item in val if isinstance(item, dict)])
|
|
113
|
+
if papers:
|
|
114
|
+
return papers
|
|
115
|
+
return []
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def format_collection_papers(data: Any, title: str | None = None) -> str:
|
|
119
|
+
papers = _extract_collection_papers(data)
|
|
120
|
+
if papers:
|
|
121
|
+
return format_table(papers, title)
|
|
122
|
+
return format_table(data, title)
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from typing import Any
|
|
6
|
+
import json
|
|
7
|
+
from datetime import datetime, timezone
|
|
6
8
|
|
|
7
9
|
from rich.console import Console
|
|
8
10
|
from rich.table import Table
|
|
@@ -17,9 +19,41 @@ def _get_authors(paper: dict[str, Any]) -> str:
|
|
|
17
19
|
names.append(a)
|
|
18
20
|
elif isinstance(a, dict):
|
|
19
21
|
names.append(a.get("name") or a.get("author") or "")
|
|
20
|
-
return ", ".join([n for n in names if n])
|
|
22
|
+
return _truncate_text(", ".join([n for n in names if n]), 72)
|
|
21
23
|
if isinstance(authors, str):
|
|
22
|
-
|
|
24
|
+
result = authors
|
|
25
|
+
else:
|
|
26
|
+
result = ""
|
|
27
|
+
return _truncate_text(result, 72)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _truncate_text(text: str, max_len: int) -> str:
|
|
31
|
+
if len(text) <= max_len:
|
|
32
|
+
return text
|
|
33
|
+
if max_len <= 3:
|
|
34
|
+
return text[:max_len]
|
|
35
|
+
return text[: max_len - 3] + "..."
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _get_year(paper: dict[str, Any]) -> str:
|
|
39
|
+
year = paper.get("year") or paper.get("publication_year") or paper.get("conference_year")
|
|
40
|
+
if year is not None:
|
|
41
|
+
if isinstance(year, float) and year.is_integer():
|
|
42
|
+
return str(int(year))
|
|
43
|
+
return str(year)
|
|
44
|
+
|
|
45
|
+
publication_date = paper.get("publication_date")
|
|
46
|
+
if isinstance(publication_date, str) and len(publication_date) >= 4 and publication_date[:4].isdigit():
|
|
47
|
+
return publication_date[:4]
|
|
48
|
+
if isinstance(publication_date, (int, float)):
|
|
49
|
+
try:
|
|
50
|
+
# Handle epoch milliseconds seen in some API payloads.
|
|
51
|
+
ts = float(publication_date)
|
|
52
|
+
if ts > 10_000_000_000:
|
|
53
|
+
ts /= 1000.0
|
|
54
|
+
return str(datetime.fromtimestamp(ts, tz=timezone.utc).year)
|
|
55
|
+
except Exception:
|
|
56
|
+
return ""
|
|
23
57
|
return ""
|
|
24
58
|
|
|
25
59
|
|
|
@@ -34,33 +68,67 @@ def _extract_papers(data: Any) -> list[dict[str, Any]]:
|
|
|
34
68
|
return []
|
|
35
69
|
|
|
36
70
|
|
|
37
|
-
def
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
71
|
+
def _format_scalar(value: Any) -> str:
|
|
72
|
+
if isinstance(value, (dict, list)):
|
|
73
|
+
return json.dumps(value, ensure_ascii=True)
|
|
74
|
+
return str(value)
|
|
75
|
+
|
|
41
76
|
|
|
77
|
+
def _format_kv_table(data: dict[str, Any], title: str | None = None) -> str:
|
|
42
78
|
table = Table(title=title)
|
|
43
|
-
table.add_column("
|
|
44
|
-
table.add_column("
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
year_val = str(p.get("year") or p.get("publication_year") or "")
|
|
53
|
-
venue_val = str(p.get("venue") or p.get("conference") or p.get("journal") or "")
|
|
54
|
-
pid = str(
|
|
55
|
-
p.get("paper_id")
|
|
56
|
-
or p.get("paperId")
|
|
57
|
-
or p.get("id")
|
|
58
|
-
or p.get("corpusid")
|
|
59
|
-
or ""
|
|
60
|
-
)
|
|
61
|
-
table.add_row(title_val, authors_val, year_val, venue_val, pid)
|
|
79
|
+
table.add_column("Field", overflow="fold")
|
|
80
|
+
table.add_column("Value", overflow="fold")
|
|
81
|
+
for key, value in data.items():
|
|
82
|
+
table.add_row(str(key), _format_scalar(value))
|
|
83
|
+
console = Console()
|
|
84
|
+
with console.capture() as capture:
|
|
85
|
+
console.print(table)
|
|
86
|
+
return capture.get()
|
|
87
|
+
|
|
62
88
|
|
|
89
|
+
def _format_list_table(data: list[Any], title: str | None = None) -> str:
|
|
90
|
+
table = Table(title=title)
|
|
91
|
+
table.add_column("#", justify="right")
|
|
92
|
+
table.add_column("Value", overflow="fold")
|
|
93
|
+
for idx, value in enumerate(data, start=1):
|
|
94
|
+
table.add_row(str(idx), _format_scalar(value))
|
|
63
95
|
console = Console()
|
|
64
96
|
with console.capture() as capture:
|
|
65
97
|
console.print(table)
|
|
66
98
|
return capture.get()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def format_table(data: Any, title: str | None = None) -> str:
|
|
102
|
+
papers = _extract_papers(data)
|
|
103
|
+
if papers:
|
|
104
|
+
table = Table(title=title, show_lines=True, row_styles=["", "dim"])
|
|
105
|
+
table.add_column("Title", overflow="fold", max_width=68)
|
|
106
|
+
table.add_column("Authors", overflow="fold", max_width=56)
|
|
107
|
+
table.add_column("Year", justify="right")
|
|
108
|
+
table.add_column("Venue", overflow="fold")
|
|
109
|
+
table.add_column("ID", overflow="fold")
|
|
110
|
+
|
|
111
|
+
for p in papers:
|
|
112
|
+
title_val = str(p.get("title") or p.get("paper_title") or "")
|
|
113
|
+
authors_val = _get_authors(p)
|
|
114
|
+
year_val = _get_year(p)
|
|
115
|
+
venue_val = str(p.get("venue") or p.get("conference") or p.get("journal") or "")
|
|
116
|
+
pid = str(
|
|
117
|
+
p.get("paper_id")
|
|
118
|
+
or p.get("paperId")
|
|
119
|
+
or p.get("id")
|
|
120
|
+
or p.get("corpusid")
|
|
121
|
+
or ""
|
|
122
|
+
)
|
|
123
|
+
table.add_row(title_val, authors_val, year_val, venue_val, pid)
|
|
124
|
+
|
|
125
|
+
console = Console()
|
|
126
|
+
with console.capture() as capture:
|
|
127
|
+
console.print(table)
|
|
128
|
+
return capture.get()
|
|
129
|
+
|
|
130
|
+
if isinstance(data, dict) and data:
|
|
131
|
+
return _format_kv_table(data, title=title)
|
|
132
|
+
if isinstance(data, list) and data:
|
|
133
|
+
return _format_list_table(data, title=title)
|
|
134
|
+
return "(no results)"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Service helpers that keep command handlers small."""
|