crossref-local 0.3.0__py3-none-any.whl → 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,179 @@
1
+ """CLI commands for cache management.
2
+
3
+ This module provides cache-related CLI commands that are registered
4
+ with the main CLI application.
5
+ """
6
+
7
+ import json
8
+ import click
9
+
10
+
11
+ def register_cache_commands(cli_group):
12
+ """Register cache commands with the CLI group."""
13
+
14
+ @cli_group.group()
15
+ def cache():
16
+ """Manage paper caches for efficient querying."""
17
+ pass
18
+
19
+ @cache.command("create")
20
+ @click.argument("name")
21
+ @click.option("-q", "--query", required=True, help="FTS search query")
22
+ @click.option("-l", "--limit", default=1000, help="Max papers to cache")
23
+ @click.option("--json", "as_json", is_flag=True, help="Output as JSON")
24
+ def cache_create(name, query, limit, as_json):
25
+ """Create a cache from search query.
26
+
27
+ Example:
28
+ crossref-local cache create epilepsy -q "epilepsy seizure" -l 500
29
+ """
30
+ from . import cache as cache_module
31
+
32
+ info = cache_module.create(name, query=query, limit=limit)
33
+ if as_json:
34
+ click.echo(json.dumps(info.to_dict(), indent=2))
35
+ else:
36
+ click.echo(f"Created cache: {info.name}")
37
+ click.echo(f" Papers: {info.paper_count}")
38
+ click.echo(f" Size: {info.size_bytes / 1024 / 1024:.2f} MB")
39
+ click.echo(f" Path: {info.path}")
40
+
41
+ @cache.command("list")
42
+ @click.option("--json", "as_json", is_flag=True, help="Output as JSON")
43
+ def cache_list(as_json):
44
+ """List all available caches."""
45
+ from . import cache as cache_module
46
+
47
+ caches = cache_module.list_caches()
48
+ if as_json:
49
+ click.echo(json.dumps([c.to_dict() for c in caches], indent=2))
50
+ else:
51
+ if not caches:
52
+ click.echo("No caches found.")
53
+ return
54
+ for c in caches:
55
+ click.echo(
56
+ f"{c.name}: {c.paper_count} papers, {c.size_bytes / 1024 / 1024:.2f} MB"
57
+ )
58
+
59
+ @cache.command("query")
60
+ @click.argument("name")
61
+ @click.option("-f", "--fields", help="Comma-separated field list")
62
+ @click.option("--abstract", is_flag=True, help="Include abstracts")
63
+ @click.option("--refs", is_flag=True, help="Include references")
64
+ @click.option("--citations", is_flag=True, help="Include citation counts")
65
+ @click.option("--year-min", type=int, help="Minimum year filter")
66
+ @click.option("--year-max", type=int, help="Maximum year filter")
67
+ @click.option("--journal", help="Journal name filter")
68
+ @click.option("-l", "--limit", type=int, help="Max results")
69
+ @click.option("--json", "as_json", is_flag=True, help="Output as JSON")
70
+ def cache_query(
71
+ name,
72
+ fields,
73
+ abstract,
74
+ refs,
75
+ citations,
76
+ year_min,
77
+ year_max,
78
+ journal,
79
+ limit,
80
+ as_json,
81
+ ):
82
+ """Query cache with field filtering.
83
+
84
+ Examples:
85
+ crossref-local cache query epilepsy -f doi,title,year
86
+ crossref-local cache query epilepsy --year-min 2020 --citations
87
+ """
88
+ from . import cache as cache_module
89
+
90
+ field_list = fields.split(",") if fields else None
91
+ papers = cache_module.query(
92
+ name,
93
+ fields=field_list,
94
+ include_abstract=abstract,
95
+ include_references=refs,
96
+ include_citations=citations,
97
+ year_min=year_min,
98
+ year_max=year_max,
99
+ journal=journal,
100
+ limit=limit,
101
+ )
102
+
103
+ if as_json:
104
+ click.echo(json.dumps(papers, indent=2))
105
+ else:
106
+ click.echo(f"Found {len(papers)} papers")
107
+ for p in papers[:10]:
108
+ title = p.get("title", "No title")[:60]
109
+ year = p.get("year", "?")
110
+ click.echo(f" [{year}] {title}...")
111
+ if len(papers) > 10:
112
+ click.echo(f" ... and {len(papers) - 10} more")
113
+
114
+ @cache.command("stats")
115
+ @click.argument("name")
116
+ @click.option("--json", "as_json", is_flag=True, help="Output as JSON")
117
+ def cache_stats(name, as_json):
118
+ """Show cache statistics."""
119
+ from . import cache as cache_module
120
+
121
+ stats = cache_module.stats(name)
122
+ if as_json:
123
+ click.echo(json.dumps(stats, indent=2))
124
+ else:
125
+ click.echo(f"Papers: {stats['paper_count']}")
126
+ yr = stats.get("year_range", {})
127
+ click.echo(f"Years: {yr.get('min', '?')} - {yr.get('max', '?')}")
128
+ click.echo(f"Abstracts: {stats['abstract_coverage']}%")
129
+ click.echo("\nTop journals:")
130
+ for j in stats.get("top_journals", [])[:5]:
131
+ click.echo(f" {j['journal']}: {j['count']}")
132
+
133
+ @cache.command("export")
134
+ @click.argument("name")
135
+ @click.argument("output")
136
+ @click.option(
137
+ "--format", "fmt", default="json", help="Format: json, csv, bibtex, dois"
138
+ )
139
+ @click.option("-f", "--fields", help="Comma-separated field list")
140
+ def cache_export(name, output, fmt, fields):
141
+ """Export cache to file.
142
+
143
+ Examples:
144
+ crossref-local cache export epilepsy papers.csv --format csv
145
+ crossref-local cache export epilepsy refs.bib --format bibtex
146
+ """
147
+ from . import cache as cache_module
148
+
149
+ field_list = fields.split(",") if fields else None
150
+ path = cache_module.export(name, output, format=fmt, fields=field_list)
151
+ click.echo(f"Exported to: {path}")
152
+
153
+ @cache.command("delete")
154
+ @click.argument("name")
155
+ @click.option("--yes", is_flag=True, help="Skip confirmation")
156
+ def cache_delete(name, yes):
157
+ """Delete a cache."""
158
+ from . import cache as cache_module
159
+
160
+ if not yes:
161
+ if not click.confirm(f"Delete cache '{name}'?"):
162
+ return
163
+
164
+ if cache_module.delete(name):
165
+ click.echo(f"Deleted: {name}")
166
+ else:
167
+ click.echo(f"Cache not found: {name}")
168
+
169
+ @cache.command("dois")
170
+ @click.argument("name")
171
+ def cache_dois(name):
172
+ """Output DOIs from cache (one per line)."""
173
+ from . import cache as cache_module
174
+
175
+ dois = cache_module.query_dois(name)
176
+ for doi in dois:
177
+ click.echo(doi)
178
+
179
+ return cache
@@ -0,0 +1,245 @@
1
+ """Shell completion commands for crossref-local CLI."""
2
+
3
+ import os
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ import click
8
+
9
+ PROG_NAME = "crossref-local"
10
+
11
+ # Shell completion scripts
12
+ BASH_COMPLETION = f"""# {PROG_NAME} bash completion
13
+ eval "$(_CROSSREF_LOCAL_COMPLETE=bash_source {PROG_NAME})"
14
+ """
15
+
16
+ ZSH_COMPLETION = f"""# {PROG_NAME} zsh completion
17
+ eval "$(_CROSSREF_LOCAL_COMPLETE=zsh_source {PROG_NAME})"
18
+ """
19
+
20
+ FISH_COMPLETION = f"""# {PROG_NAME} fish completion
21
+ _CROSSREF_LOCAL_COMPLETE=fish_source {PROG_NAME} | source
22
+ """
23
+
24
+ # Shell config files
25
+ SHELL_CONFIGS = {
26
+ "bash": [Path.home() / ".bashrc", Path.home() / ".bash_profile"],
27
+ "zsh": [Path.home() / ".zshrc"],
28
+ "fish": [Path.home() / ".config" / "fish" / "config.fish"],
29
+ }
30
+
31
+ COMPLETION_SCRIPTS = {
32
+ "bash": BASH_COMPLETION,
33
+ "zsh": ZSH_COMPLETION,
34
+ "fish": FISH_COMPLETION,
35
+ }
36
+
37
+ COMPLETION_MARKER = f"# >>> {PROG_NAME} completion >>>"
38
+ COMPLETION_END_MARKER = f"# <<< {PROG_NAME} completion <<<"
39
+
40
+
41
+ def _detect_shell() -> str:
42
+ """Detect current shell from $SHELL environment variable."""
43
+ shell_path = os.environ.get("SHELL", "")
44
+ shell_name = Path(shell_path).name if shell_path else ""
45
+
46
+ if shell_name in ("bash", "zsh", "fish"):
47
+ return shell_name
48
+
49
+ # Fallback to bash
50
+ return "bash"
51
+
52
+
53
+ def _get_config_file(shell: str) -> Path | None:
54
+ """Get the appropriate config file for the shell."""
55
+ configs = SHELL_CONFIGS.get(shell, [])
56
+ for config in configs:
57
+ if config.exists():
58
+ return config
59
+ # Return first option for creation
60
+ return configs[0] if configs else None
61
+
62
+
63
+ def _is_installed(shell: str) -> tuple[bool, Path | None]:
64
+ """Check if completion is already installed for a shell."""
65
+ configs = SHELL_CONFIGS.get(shell, [])
66
+ for config in configs:
67
+ if config.exists():
68
+ content = config.read_text()
69
+ if COMPLETION_MARKER in content:
70
+ return True, config
71
+ return False, None
72
+
73
+
74
+ def _install_completion(shell: str) -> tuple[bool, str]:
75
+ """Install completion for a shell. Returns (success, message)."""
76
+ installed, existing_file = _is_installed(shell)
77
+ if installed:
78
+ return True, f"Already installed in {existing_file}"
79
+
80
+ config_file = _get_config_file(shell)
81
+ if config_file is None:
82
+ return False, f"Could not find config file for {shell}"
83
+
84
+ script = COMPLETION_SCRIPTS.get(shell)
85
+ if script is None:
86
+ return False, f"Unsupported shell: {shell}"
87
+
88
+ # Create parent directory if needed (for fish)
89
+ config_file.parent.mkdir(parents=True, exist_ok=True)
90
+
91
+ # Append completion to config file
92
+ completion_block = f"\n{COMPLETION_MARKER}\n{script}{COMPLETION_END_MARKER}\n"
93
+
94
+ with open(config_file, "a") as f:
95
+ f.write(completion_block)
96
+
97
+ return True, f"Installed to {config_file}"
98
+
99
+
100
+ def _uninstall_completion(shell: str) -> tuple[bool, str]:
101
+ """Uninstall completion for a shell. Returns (success, message)."""
102
+ installed, config_file = _is_installed(shell)
103
+ if not installed:
104
+ return True, f"Not installed for {shell}"
105
+
106
+ if config_file is None:
107
+ return False, f"Could not find config file for {shell}"
108
+
109
+ content = config_file.read_text()
110
+
111
+ # Remove the completion block
112
+ start_idx = content.find(COMPLETION_MARKER)
113
+ end_idx = content.find(COMPLETION_END_MARKER)
114
+
115
+ if start_idx == -1 or end_idx == -1:
116
+ return False, "Could not find completion block to remove"
117
+
118
+ # Include the newline before marker and after end marker
119
+ if start_idx > 0 and content[start_idx - 1] == "\n":
120
+ start_idx -= 1
121
+ end_idx = end_idx + len(COMPLETION_END_MARKER)
122
+ if end_idx < len(content) and content[end_idx] == "\n":
123
+ end_idx += 1
124
+
125
+ new_content = content[:start_idx] + content[end_idx:]
126
+ config_file.write_text(new_content)
127
+
128
+ return True, f"Removed from {config_file}"
129
+
130
+
131
+ @click.group("completion", invoke_without_command=True)
132
+ @click.pass_context
133
+ def completion(ctx):
134
+ """Shell completion commands.
135
+
136
+ \b
137
+ Install shell tab-completion for crossref-local CLI.
138
+ Running without subcommand auto-installs for detected shell.
139
+
140
+ \b
141
+ Examples:
142
+ crossref-local completion # Auto-install for current shell
143
+ crossref-local completion status # Check installation status
144
+ crossref-local completion bash # Show bash completion script
145
+ """
146
+ if ctx.invoked_subcommand is None:
147
+ # Auto-install for detected shell
148
+ shell = _detect_shell()
149
+ click.echo(f"Detected shell: {shell}")
150
+
151
+ success, message = _install_completion(shell)
152
+ if success:
153
+ click.echo(f"[OK] {message}")
154
+ click.echo(
155
+ f"\nRestart your shell or run: source ~/{_get_config_file(shell).name}"
156
+ )
157
+ else:
158
+ click.echo(f"[ERROR] {message}", err=True)
159
+ sys.exit(1)
160
+
161
+
162
+ @completion.command("install")
163
+ @click.option(
164
+ "--shell",
165
+ type=click.Choice(["bash", "zsh", "fish"]),
166
+ default=None,
167
+ help="Shell to install completion for (default: auto-detect)",
168
+ )
169
+ def install_cmd(shell: str | None):
170
+ """Install completion to shell config file."""
171
+ if shell is None:
172
+ shell = _detect_shell()
173
+ click.echo(f"Detected shell: {shell}")
174
+
175
+ success, message = _install_completion(shell)
176
+ if success:
177
+ click.echo(f"[OK] {message}")
178
+ config_file = _get_config_file(shell)
179
+ if config_file:
180
+ click.echo(f"\nRestart your shell or run: source {config_file}")
181
+ else:
182
+ click.echo(f"[ERROR] {message}", err=True)
183
+ sys.exit(1)
184
+
185
+
186
+ @completion.command("uninstall")
187
+ @click.option(
188
+ "--shell",
189
+ type=click.Choice(["bash", "zsh", "fish"]),
190
+ default=None,
191
+ help="Shell to uninstall completion from (default: auto-detect)",
192
+ )
193
+ def uninstall_cmd(shell: str | None):
194
+ """Remove completion from shell config file."""
195
+ if shell is None:
196
+ shell = _detect_shell()
197
+ click.echo(f"Detected shell: {shell}")
198
+
199
+ success, message = _uninstall_completion(shell)
200
+ if success:
201
+ click.echo(f"[OK] {message}")
202
+ else:
203
+ click.echo(f"[ERROR] {message}", err=True)
204
+ sys.exit(1)
205
+
206
+
207
+ @completion.command("status")
208
+ def status_cmd():
209
+ """Check completion installation status for all shells."""
210
+ click.echo(f"{PROG_NAME} Shell Completion Status")
211
+ click.echo("=" * 40)
212
+
213
+ current_shell = _detect_shell()
214
+ click.echo(f"Current shell: {current_shell}")
215
+ click.echo()
216
+
217
+ for shell in ["bash", "zsh", "fish"]:
218
+ installed, config_file = _is_installed(shell)
219
+ marker = "[x]" if installed else "[ ]"
220
+ location = f" ({config_file})" if installed and config_file else ""
221
+ current = " (current)" if shell == current_shell else ""
222
+ click.echo(f" {marker} {shell}{current}{location}")
223
+
224
+
225
+ @completion.command("bash")
226
+ def bash_cmd():
227
+ """Show bash completion script."""
228
+ click.echo(BASH_COMPLETION.strip())
229
+
230
+
231
+ @completion.command("zsh")
232
+ def zsh_cmd():
233
+ """Show zsh completion script."""
234
+ click.echo(ZSH_COMPLETION.strip())
235
+
236
+
237
+ @completion.command("fish")
238
+ def fish_cmd():
239
+ """Show fish completion script."""
240
+ click.echo(FISH_COMPLETION.strip())
241
+
242
+
243
+ def register_completion_commands(cli_group):
244
+ """Register completion commands with the main CLI group."""
245
+ cli_group.add_command(completion)
@@ -0,0 +1,20 @@
1
+ """Main CLI entry point with all command groups registered."""
2
+
3
+ from .cli import cli
4
+ from .cli_cache import register_cache_commands
5
+ from .cli_completion import register_completion_commands
6
+ from .cli_mcp import register_mcp_commands
7
+
8
+ # Register command groups
9
+ register_cache_commands(cli)
10
+ register_completion_commands(cli)
11
+ register_mcp_commands(cli)
12
+
13
+
14
+ def main():
15
+ """Entry point for CLI."""
16
+ cli()
17
+
18
+
19
+ if __name__ == "__main__":
20
+ main()