scholarinboxcli 0.1.0__py3-none-any.whl → 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- scholarinboxcli/__init__.py +1 -1
- scholarinboxcli/api/client.py +32 -56
- scholarinboxcli/api/endpoints.py +54 -0
- scholarinboxcli/cli.py +11 -505
- scholarinboxcli/commands/__init__.py +1 -0
- scholarinboxcli/commands/auth.py +38 -0
- scholarinboxcli/commands/bookmarks.py +48 -0
- scholarinboxcli/commands/collections.py +130 -0
- scholarinboxcli/commands/common.py +53 -0
- scholarinboxcli/commands/conferences.py +34 -0
- scholarinboxcli/commands/papers.py +88 -0
- scholarinboxcli/services/__init__.py +1 -0
- scholarinboxcli/services/collections.py +132 -0
- {scholarinboxcli-0.1.0.dist-info → scholarinboxcli-0.1.1.dist-info}/METADATA +8 -1
- scholarinboxcli-0.1.1.dist-info/RECORD +21 -0
- scholarinboxcli-0.1.1.dist-info/licenses/LICENSE +21 -0
- scholarinboxcli-0.1.0.dist-info/RECORD +0 -10
- {scholarinboxcli-0.1.0.dist-info → scholarinboxcli-0.1.1.dist-info}/WHEEL +0 -0
- {scholarinboxcli-0.1.0.dist-info → scholarinboxcli-0.1.1.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,130 @@
|
|
|
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.services.collections import resolve_collection_id
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
app = typer.Typer(help="Collection commands", no_args_is_help=True)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@app.command("list")
|
|
17
|
+
def collection_list(
|
|
18
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
19
|
+
expanded: bool = typer.Option(False, "--expanded", help="Use expanded collection metadata"),
|
|
20
|
+
no_retry: bool = typer.Option(False, "--no-retry", help="Disable retry on rate limits"),
|
|
21
|
+
):
|
|
22
|
+
def action(client):
|
|
23
|
+
data = client.collections_expanded() if expanded else client.collections_list()
|
|
24
|
+
print_output(data, json_output, title="Collections")
|
|
25
|
+
|
|
26
|
+
with_client(no_retry, action)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@app.command("create")
|
|
30
|
+
def collection_create(
|
|
31
|
+
name: str = typer.Argument(..., help="Collection name"),
|
|
32
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
33
|
+
no_retry: bool = typer.Option(False, "--no-retry", help="Disable retry on rate limits"),
|
|
34
|
+
):
|
|
35
|
+
def action(client):
|
|
36
|
+
data = client.collection_create(name)
|
|
37
|
+
print_output(data, json_output, title="Collection created")
|
|
38
|
+
|
|
39
|
+
with_client(no_retry, action)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@app.command("rename")
|
|
43
|
+
def collection_rename(
|
|
44
|
+
collection_id: str = typer.Argument(..., help="Collection ID or name"),
|
|
45
|
+
new_name: str = typer.Argument(..., help="New collection name"),
|
|
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
|
+
cid = resolve_collection_id(client, collection_id)
|
|
51
|
+
data = client.collection_rename(cid, new_name)
|
|
52
|
+
print_output(data, json_output, title="Collection renamed")
|
|
53
|
+
|
|
54
|
+
with_client(no_retry, action)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@app.command("delete")
|
|
58
|
+
def collection_delete(
|
|
59
|
+
collection_id: str = typer.Argument(..., help="Collection ID or name"),
|
|
60
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
61
|
+
no_retry: bool = typer.Option(False, "--no-retry", help="Disable retry on rate limits"),
|
|
62
|
+
):
|
|
63
|
+
def action(client):
|
|
64
|
+
cid = resolve_collection_id(client, collection_id)
|
|
65
|
+
data = client.collection_delete(cid)
|
|
66
|
+
print_output(data, json_output, title="Collection deleted")
|
|
67
|
+
|
|
68
|
+
with_client(no_retry, action)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@app.command("add")
|
|
72
|
+
def collection_add(
|
|
73
|
+
collection_id: str = typer.Argument(..., help="Collection ID or name"),
|
|
74
|
+
paper_id: str = typer.Argument(..., help="Paper ID"),
|
|
75
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
76
|
+
no_retry: bool = typer.Option(False, "--no-retry", help="Disable retry on rate limits"),
|
|
77
|
+
):
|
|
78
|
+
def action(client):
|
|
79
|
+
cid = resolve_collection_id(client, collection_id)
|
|
80
|
+
data = client.collection_add_paper(cid, paper_id)
|
|
81
|
+
print_output(data, json_output, title="Collection add paper")
|
|
82
|
+
|
|
83
|
+
with_client(no_retry, action)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@app.command("remove")
|
|
87
|
+
def collection_remove(
|
|
88
|
+
collection_id: str = typer.Argument(..., help="Collection ID or name"),
|
|
89
|
+
paper_id: str = typer.Argument(..., help="Paper ID"),
|
|
90
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
91
|
+
no_retry: bool = typer.Option(False, "--no-retry", help="Disable retry on rate limits"),
|
|
92
|
+
):
|
|
93
|
+
def action(client):
|
|
94
|
+
cid = resolve_collection_id(client, collection_id)
|
|
95
|
+
data = client.collection_remove_paper(cid, paper_id)
|
|
96
|
+
print_output(data, json_output, title="Collection remove paper")
|
|
97
|
+
|
|
98
|
+
with_client(no_retry, action)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@app.command("papers")
|
|
102
|
+
def collection_papers(
|
|
103
|
+
collection_id: str = typer.Argument(..., help="Collection ID or name"),
|
|
104
|
+
limit: Optional[int] = typer.Option(None, "--limit", "-n", help="Limit results"),
|
|
105
|
+
offset: Optional[int] = typer.Option(None, "--offset", help="Pagination offset"),
|
|
106
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
107
|
+
no_retry: bool = typer.Option(False, "--no-retry", help="Disable retry on rate limits"),
|
|
108
|
+
):
|
|
109
|
+
def action(client):
|
|
110
|
+
cid = resolve_collection_id(client, collection_id)
|
|
111
|
+
data = client.collection_papers(cid, limit=limit, offset=offset)
|
|
112
|
+
print_output(data, json_output, title=f"Collection {cid}")
|
|
113
|
+
|
|
114
|
+
with_client(no_retry, action)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@app.command("similar")
|
|
118
|
+
def collection_similar(
|
|
119
|
+
collection_ids: list[str] = typer.Argument(..., help="Collection ID(s) or names"),
|
|
120
|
+
limit: Optional[int] = typer.Option(None, "--limit", "-n", help="Limit results"),
|
|
121
|
+
offset: Optional[int] = typer.Option(None, "--offset", help="Pagination offset"),
|
|
122
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
123
|
+
no_retry: bool = typer.Option(False, "--no-retry", help="Disable retry on rate limits"),
|
|
124
|
+
):
|
|
125
|
+
def action(client):
|
|
126
|
+
resolved = [resolve_collection_id(client, cid) for cid in collection_ids]
|
|
127
|
+
data = client.collections_similar(resolved, limit=limit, offset=offset)
|
|
128
|
+
print_output(data, json_output, title="Similar Papers")
|
|
129
|
+
|
|
130
|
+
with_client(no_retry, action)
|
|
@@ -0,0 +1,53 @@
|
|
|
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(data: Any, use_json: bool, title: str | None = None) -> None:
|
|
16
|
+
if use_json or not sys.stdout.isatty():
|
|
17
|
+
typer.echo(format_json(data))
|
|
18
|
+
return
|
|
19
|
+
|
|
20
|
+
table = format_table(data, title=title)
|
|
21
|
+
if table == "(no results)":
|
|
22
|
+
typer.echo(table)
|
|
23
|
+
return
|
|
24
|
+
typer.echo(table)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def handle_error(err: ApiError) -> None:
|
|
28
|
+
if not sys.stdout.isatty():
|
|
29
|
+
typer.echo(format_json({"error": err.message, "status_code": err.status_code, "detail": err.detail}))
|
|
30
|
+
else:
|
|
31
|
+
typer.echo(f"Error: {err.message}", err=True)
|
|
32
|
+
if err.status_code:
|
|
33
|
+
typer.echo(f"Status: {err.status_code}", err=True)
|
|
34
|
+
raise typer.Exit(1)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def close_client(client: ScholarInboxClient) -> None:
|
|
38
|
+
client.close()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
T = TypeVar("T")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def with_client(no_retry: bool, action: Callable[[ScholarInboxClient], T]) -> T:
|
|
45
|
+
"""Run action with a managed client and standardized ApiError handling."""
|
|
46
|
+
client = ScholarInboxClient(no_retry=no_retry)
|
|
47
|
+
try:
|
|
48
|
+
return action(client)
|
|
49
|
+
except ApiError as err:
|
|
50
|
+
handle_error(err)
|
|
51
|
+
raise # unreachable, keeps type-checkers happy
|
|
52
|
+
finally:
|
|
53
|
+
close_client(client)
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
|
|
9
|
+
|
|
10
|
+
app = typer.Typer(help="Conference commands", no_args_is_help=True)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@app.command("list")
|
|
14
|
+
def conference_list(
|
|
15
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
16
|
+
no_retry: bool = typer.Option(False, "--no-retry", help="Disable retry on rate limits"),
|
|
17
|
+
):
|
|
18
|
+
def action(client):
|
|
19
|
+
data = client.conference_list()
|
|
20
|
+
print_output(data, json_output, title="Conferences")
|
|
21
|
+
|
|
22
|
+
with_client(no_retry, action)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@app.command("explore")
|
|
26
|
+
def conference_explore(
|
|
27
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
28
|
+
no_retry: bool = typer.Option(False, "--no-retry", help="Disable retry on rate limits"),
|
|
29
|
+
):
|
|
30
|
+
def action(client):
|
|
31
|
+
data = client.conference_explorer()
|
|
32
|
+
print_output(data, json_output, title="Conference Explorer")
|
|
33
|
+
|
|
34
|
+
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 @@
|
|
|
1
|
+
"""Service helpers that keep command handlers small."""
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Collection name/ID resolution helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from scholarinboxcli.api.client import ApiError, ScholarInboxClient
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def normalize_name(name: str) -> str:
|
|
11
|
+
return name.strip().lower()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def collection_candidates(items: object) -> list[tuple[str, str]]:
|
|
15
|
+
if not isinstance(items, list):
|
|
16
|
+
return []
|
|
17
|
+
candidates: list[tuple[str, str]] = []
|
|
18
|
+
for item in items:
|
|
19
|
+
if isinstance(item, dict):
|
|
20
|
+
name = item.get("name") or item.get("collection_name") or ""
|
|
21
|
+
cid = str(item.get("id") or item.get("collection_id") or "")
|
|
22
|
+
elif isinstance(item, str):
|
|
23
|
+
name = item
|
|
24
|
+
cid = ""
|
|
25
|
+
else:
|
|
26
|
+
continue
|
|
27
|
+
if name:
|
|
28
|
+
candidates.append((name, cid))
|
|
29
|
+
return candidates
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def collection_items_from_response(data: object) -> object:
|
|
33
|
+
if isinstance(data, dict):
|
|
34
|
+
for key in ("collections", "expanded_collections", "collection_names"):
|
|
35
|
+
if key in data:
|
|
36
|
+
return data.get(key)
|
|
37
|
+
return data
|
|
38
|
+
return data
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def collection_candidates_from_map(data: object) -> list[tuple[str, str]]:
|
|
42
|
+
if not isinstance(data, dict):
|
|
43
|
+
return []
|
|
44
|
+
mapping = data.get("collection_names_to_ids_dict")
|
|
45
|
+
if not isinstance(mapping, dict):
|
|
46
|
+
return []
|
|
47
|
+
return [(str(name), str(cid)) for name, cid in mapping.items() if name and cid is not None]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def candidates_have_ids(candidates: list[tuple[str, str]]) -> bool:
|
|
51
|
+
return any(cid for _, cid in candidates)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def match_collection_name(candidates: list[tuple[str, str]], identifier: str) -> str | None:
|
|
55
|
+
target = normalize_name(identifier)
|
|
56
|
+
names = [(name, cid) for name, cid in candidates if name]
|
|
57
|
+
|
|
58
|
+
for name, _ in names:
|
|
59
|
+
if normalize_name(name) == target:
|
|
60
|
+
return name
|
|
61
|
+
|
|
62
|
+
prefix = [c for c in names if normalize_name(c[0]).startswith(target)]
|
|
63
|
+
if len(prefix) == 1:
|
|
64
|
+
return prefix[0][0]
|
|
65
|
+
if len(prefix) > 1:
|
|
66
|
+
names_str = ", ".join([n for n, _ in prefix[:10]])
|
|
67
|
+
raise ApiError(f"Ambiguous collection name. Matches: {names_str}")
|
|
68
|
+
|
|
69
|
+
contains = [c for c in names if target in normalize_name(c[0])]
|
|
70
|
+
if len(contains) == 1:
|
|
71
|
+
return contains[0][0]
|
|
72
|
+
if len(contains) > 1:
|
|
73
|
+
names_str = ", ".join([n for n, _ in contains[:10]])
|
|
74
|
+
raise ApiError(f"Ambiguous collection name. Matches: {names_str}")
|
|
75
|
+
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def resolve_collection_id(client: ScholarInboxClient, identifier: str) -> str:
|
|
80
|
+
"""Resolve numeric IDs directly, otherwise match by collection name."""
|
|
81
|
+
if identifier.isdigit():
|
|
82
|
+
return identifier
|
|
83
|
+
|
|
84
|
+
data = client.collections_list()
|
|
85
|
+
items = collection_items_from_response(data)
|
|
86
|
+
candidates = collection_candidates(items)
|
|
87
|
+
|
|
88
|
+
if not candidates_have_ids(candidates):
|
|
89
|
+
try:
|
|
90
|
+
data = client.collections_expanded()
|
|
91
|
+
items = collection_items_from_response(data)
|
|
92
|
+
candidates = collection_candidates(items)
|
|
93
|
+
except ApiError:
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
if not candidates_have_ids(candidates):
|
|
97
|
+
try:
|
|
98
|
+
data = client.collections_map()
|
|
99
|
+
mapped = collection_candidates_from_map(data)
|
|
100
|
+
if mapped:
|
|
101
|
+
candidates = mapped
|
|
102
|
+
except ApiError:
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
if not candidates_have_ids(candidates):
|
|
106
|
+
matched = match_collection_name(candidates, identifier)
|
|
107
|
+
if matched:
|
|
108
|
+
return matched
|
|
109
|
+
raise ApiError("Unable to resolve collection name (no IDs available)")
|
|
110
|
+
|
|
111
|
+
candidates = [(name, cid) for name, cid in candidates if cid]
|
|
112
|
+
target = normalize_name(identifier)
|
|
113
|
+
|
|
114
|
+
for name, cid in candidates:
|
|
115
|
+
if normalize_name(name) == target:
|
|
116
|
+
return cid
|
|
117
|
+
|
|
118
|
+
prefix = [c for c in candidates if normalize_name(c[0]).startswith(target)]
|
|
119
|
+
if len(prefix) == 1:
|
|
120
|
+
return prefix[0][1]
|
|
121
|
+
if len(prefix) > 1:
|
|
122
|
+
names = ", ".join([f"{n}({cid})" for n, cid in prefix[:10]])
|
|
123
|
+
raise ApiError(f"Ambiguous collection name. Matches: {names}")
|
|
124
|
+
|
|
125
|
+
contains = [c for c in candidates if target in normalize_name(c[0])]
|
|
126
|
+
if len(contains) == 1:
|
|
127
|
+
return contains[0][1]
|
|
128
|
+
if len(contains) > 1:
|
|
129
|
+
names = ", ".join([f"{n}({cid})" for n, cid in contains[:10]])
|
|
130
|
+
raise ApiError(f"Ambiguous collection name. Matches: {names}")
|
|
131
|
+
|
|
132
|
+
raise ApiError("Collection name not found")
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scholarinboxcli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: CLI for Scholar Inbox (authenticated web API)
|
|
5
5
|
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
6
7
|
Keywords: bibliography,cli,research,scholar
|
|
7
8
|
Classifier: Development Status :: 3 - Alpha
|
|
8
9
|
Classifier: Environment :: Console
|
|
@@ -234,3 +235,9 @@ If using an API token:
|
|
|
234
235
|
export TWINE_USERNAME=__token__
|
|
235
236
|
export TWINE_PASSWORD=<your-pypi-token>
|
|
236
237
|
```
|
|
238
|
+
|
|
239
|
+
Automated publish is also configured via GitHub Actions:
|
|
240
|
+
|
|
241
|
+
- Workflow: `.github/workflows/publish.yml`
|
|
242
|
+
- Trigger: push a tag matching `v*` (for example `v0.1.1`)
|
|
243
|
+
- Auth: PyPI Trusted Publishing (OIDC)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
scholarinboxcli/__init__.py,sha256=rnObPjuBcEStqSO0S6gsdS_ot8ITOQjVj_-P1LUUYpg,22
|
|
2
|
+
scholarinboxcli/cli.py,sha256=MEpkfuWXC4U_GjT85ue-LRTE_FejFvYhxwtnYbWr18k,1001
|
|
3
|
+
scholarinboxcli/config.py,sha256=cxp1RzNwzT6Iu225EPvs8NhH2YTTMg9fQBjaYIRVDoc,1545
|
|
4
|
+
scholarinboxcli/api/client.py,sha256=8Wd7CzAuku2gP2lrdvv2s81JV5Gr6pqGSggnKdk-7-8,13033
|
|
5
|
+
scholarinboxcli/api/endpoints.py,sha256=aw686_VtgbJelcGbw__cG8qtHVm_f9pGeNEqeuWeOuo,1503
|
|
6
|
+
scholarinboxcli/commands/__init__.py,sha256=aOPGu7M21finmICNxDSxNBpe1C3Vw2Iimh1UBESfekU,42
|
|
7
|
+
scholarinboxcli/commands/auth.py,sha256=D2CXLVMwJTxU3_OLNDZ9YGG-mYzrZ4agTKRLeIS3wx4,916
|
|
8
|
+
scholarinboxcli/commands/bookmarks.py,sha256=S0ah6woZrS3p_T4r-gAZZrTSr8GL2EQb6cEtaCKXD0o,1478
|
|
9
|
+
scholarinboxcli/commands/collections.py,sha256=rfSh98wT-yrDvYjl_1UcbVvEiw-h5yaHCv9WTkNHMWg,5139
|
|
10
|
+
scholarinboxcli/commands/common.py,sha256=KgudFViqscHOCfLe_KPADGr_E_d1KKBoW1ZGje3gHuU,1531
|
|
11
|
+
scholarinboxcli/commands/conferences.py,sha256=wVs-YpdkLVxlwqu3_rYHrJl4j1tIb8exvsp7t96qHqo,997
|
|
12
|
+
scholarinboxcli/commands/papers.py,sha256=mdb3-_iecy_pjavPL_K0cXfVnDsQMU93SbD8T3hJY10,3968
|
|
13
|
+
scholarinboxcli/formatters/json_fmt.py,sha256=Ntcp4EqHugCXg79RIF62c7QHa-lexptLDDTT3IEP65U,197
|
|
14
|
+
scholarinboxcli/formatters/table.py,sha256=GnzpmSJ7M_yq-R-c8no8SE9vXbycvWWPUu6hV4tcJAA,2133
|
|
15
|
+
scholarinboxcli/services/__init__.py,sha256=i-8EAvmxCDyYEjmfILYRcUHUh7ryY5hFS4PZARScFXo,56
|
|
16
|
+
scholarinboxcli/services/collections.py,sha256=mQ67tEmNwspOeWg9NyIIovpqkLMP2ANi4Jso9SHXi5A,4377
|
|
17
|
+
scholarinboxcli-0.1.1.dist-info/METADATA,sha256=E1IrW2o5X9P_-6AJDzcS8K6YvyrGrADdHBgVmAVlcPU,6285
|
|
18
|
+
scholarinboxcli-0.1.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
19
|
+
scholarinboxcli-0.1.1.dist-info/entry_points.txt,sha256=iescoEMF_CPwSNSmvlzNDl5pT2VpBL9_1bIq_FFIAKc,60
|
|
20
|
+
scholarinboxcli-0.1.1.dist-info/licenses/LICENSE,sha256=sP1DPhQGvTFx1mH4JhNuczMMbApQgFvhBcVjZKM79gU,1068
|
|
21
|
+
scholarinboxcli-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Marek Suppa
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
scholarinboxcli/__init__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
|
|
2
|
-
scholarinboxcli/cli.py,sha256=bugBfT66k5EuVPashcG6Pz0IRM0KtRz_c6lNOalm4pk,19242
|
|
3
|
-
scholarinboxcli/config.py,sha256=cxp1RzNwzT6Iu225EPvs8NhH2YTTMg9fQBjaYIRVDoc,1545
|
|
4
|
-
scholarinboxcli/api/client.py,sha256=TE8pIRXh7pHhQGto9CvRpPPT61SJp9SzNhRnQ-2U4M4,13723
|
|
5
|
-
scholarinboxcli/formatters/json_fmt.py,sha256=Ntcp4EqHugCXg79RIF62c7QHa-lexptLDDTT3IEP65U,197
|
|
6
|
-
scholarinboxcli/formatters/table.py,sha256=GnzpmSJ7M_yq-R-c8no8SE9vXbycvWWPUu6hV4tcJAA,2133
|
|
7
|
-
scholarinboxcli-0.1.0.dist-info/METADATA,sha256=JZISdlP49Ezyh-UoLv2FlJIrliHUvZrnj0IvFNWKCMw,6062
|
|
8
|
-
scholarinboxcli-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
9
|
-
scholarinboxcli-0.1.0.dist-info/entry_points.txt,sha256=iescoEMF_CPwSNSmvlzNDl5pT2VpBL9_1bIq_FFIAKc,60
|
|
10
|
-
scholarinboxcli-0.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|