File size: 7,767 Bytes
fc906cf
 
 
 
 
 
 
 
 
 
 
 
 
 
6873fcf
 
d7a80a2
6873fcf
 
 
 
 
 
 
fc906cf
d7a80a2
6873fcf
 
fc906cf
aba5c8f
fc906cf
 
1e0ae88
 
fc906cf
 
 
 
 
 
 
 
 
 
b9997e0
 
 
afd6fd6
fc906cf
9067804
 
 
fc906cf
 
 
 
 
 
 
 
 
 
 
 
 
1e0ae88
fc906cf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bdd3513
 
 
 
 
fc906cf
 
 
 
 
 
 
c8afaed
fc906cf
c8afaed
 
 
fc906cf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
384e013
 
1b407a4
867d2c5
 
 
7ea1b99
867d2c5
7ea1b99
 
fc906cf
 
 
 
 
 
 
384e013
fc906cf
 
 
 
384e013
 
 
 
1855cca
384e013
 
7ea1b99
 
867d2c5
7ea1b99
867d2c5
 
7ea1b99
fc906cf
1e0ae88
 
 
 
 
 
 
 
 
 
 
 
fc906cf
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
"""
Command-line interface for citation verification.
"""

import sys
from pathlib import Path

import click
from colorama import init, Fore, Style

from .parser import parse_bibtex_file, format_entry_summary
from .verifier import CitationVerifier


def validate_bibtex_file(ctx, param, value):
    """Validate that the BibTeX file exists."""
    if not Path(value).exists():
        raise click.BadParameter(
            f"File not found: {value}\n"
            f"Tip: Specify a BibTeX file or ensure 'references.bib' exists in the current directory"
        )
    return value


@click.command()
@click.option('--bibtex-file', default='references.bib', type=click.Path(),
              callback=validate_bibtex_file,
              help='BibTeX file to verify (default: references.bib)')
@click.option('--timeout', default=10, help='Request timeout in seconds')
@click.option('--max-retries', default=3, help='Maximum retries for 429 rate limit errors')
@click.option('--verbose', '-v', is_flag=True, help='Show detailed output')
@click.option('--summary-only', '-s', is_flag=True, help='Show only summary')
@click.option('--crossref-mailto', default='', help='Email for CrossRef polite pool (improves rate limits)')
def main(bibtex_file, timeout, max_retries, verbose, summary_only, crossref_mailto):
    """
    Verify citations in a BibTeX file.
    
    This tool checks if citations are valid by:
    1. Verifying the paper can be found online with a quick search
    2. Validating provided URLs are correct and accessible
    3. Checking title and author metadata
    4. Identifying version information (arXiv, published, etc.)
    
    Example:
        verify-citations --bibtex-file references.bib
        verify-citations --bibtex-file sample.bib --verbose
        verify-citations --max-retries 5
        verify-citations (uses default references.bib)
    """
    # Initialize colorama for cross-platform colored output
    init(autoreset=True)
    
    click.echo(f"{Fore.CYAN}=== Citation Verification Tool ==={Style.RESET_ALL}\n")
    click.echo(f"Processing: {bibtex_file}\n")
    
    try:
        # Parse BibTeX file
        entries = parse_bibtex_file(bibtex_file)
        click.echo(f"Found {len(entries)} citation(s) to verify\n")
        
        if not entries:
            click.echo(f"{Fore.YELLOW}No citations found in file{Style.RESET_ALL}")
            return
        
        # Verify each citation
        verifier = CitationVerifier(timeout=timeout, max_retries=max_retries, crossref_mailto=crossref_mailto)
        results = []
        
        for i, entry in enumerate(entries, 1):
            if not summary_only:
                click.echo(f"{Fore.CYAN}[{i}/{len(entries)}] Verifying:{Style.RESET_ALL}")
                click.echo(f"  {format_entry_summary(entry)}")
            
            result = verifier.verify_citation(entry)
            results.append(result)
            
            if not summary_only:
                _print_result(result, verbose)
                click.echo()  # Blank line between entries
        
        # Print summary
        _print_summary(results)
        
    except Exception as e:
        click.echo(f"{Fore.RED}Error: {str(e)}{Style.RESET_ALL}", err=True)
        if verbose:
            import traceback
            traceback.print_exc()
        sys.exit(1)


def _print_result(result: dict, verbose: bool):
    """Print verification result for a single citation."""
    status = result['status']
    
    # Status indicator
    if status == 'verified':
        status_msg = f"{Fore.GREEN}✓ VERIFIED{Style.RESET_ALL}"
    elif status == 'issues_found':
        status_msg = f"{Fore.RED}✗ ISSUES FOUND{Style.RESET_ALL}"
    else:
        status_msg = f"{Fore.YELLOW}⚠ INCOMPLETE{Style.RESET_ALL}"
    
    click.echo(f"  Status: {status_msg}")
    
    # Print verbose logs if in verbose mode
    if verbose and result.get('verbose_logs'):
        for log in result['verbose_logs']:
            click.echo(f"  {Fore.CYAN}{log}{Style.RESET_ALL}")
    
    # Print messages
    if verbose or result['status'] != 'verified':
        for msg in result['messages']:
            # Color code messages
            if msg.startswith('✓'):
                colored_msg = f"{Fore.GREEN}{msg}{Style.RESET_ALL}"
            elif msg.startswith('✗'):
                # Red for critical errors (URL invalid, paper not found)
                colored_msg = f"{Fore.RED}{msg}{Style.RESET_ALL}"
            elif msg.startswith('⚠'):
                # Yellow for warnings (metadata mismatches)
                colored_msg = f"{Fore.YELLOW}{msg}{Style.RESET_ALL}"
            elif msg.startswith('ℹ'):
                colored_msg = f"{Fore.CYAN}{msg}{Style.RESET_ALL}"
            else:
                colored_msg = msg
            click.echo(f"    {colored_msg}")


def _print_summary(results: list):
    """Print summary of all verification results."""
    click.echo(f"{Fore.CYAN}{'='*60}{Style.RESET_ALL}")
    click.echo(f"{Fore.CYAN}SUMMARY{Style.RESET_ALL}\n")
    
    verified = sum(1 for r in results if r['status'] == 'verified')
    issues = sum(1 for r in results if r['status'] == 'issues_found')
    incomplete = sum(1 for r in results if r['status'] == 'incomplete')
    
    # Count citations with actual 403 errors (where has_403 flag is True)
    citations_with_403 = [r for r in results if r.get('has_403', False)]
    
    # Count citations where metadata could not be verified automatically
    # This matches the count of "Could not verify metadata automatically" messages
    citations_no_metadata_verification = [
        r for r in results 
        if r.get('metadata_not_verified', False)
    ]
    
    total = len(results)
    
    click.echo(f"Total citations: {total}")
    click.echo(f"{Fore.GREEN}Verified: {verified}{Style.RESET_ALL}")
    click.echo(f"{Fore.RED}Issues found: {issues}{Style.RESET_ALL}")
    click.echo(f"{Fore.YELLOW}Incomplete: {incomplete}{Style.RESET_ALL}")
    
    # Show citations with issues (excluding those that only have 403 errors)
    if issues > 0:
        click.echo(f"\n{Fore.YELLOW}Citations with issues:{Style.RESET_ALL}")
        for result in results:
            if result['status'] == 'issues_found':
                click.echo(f"- {result['id']}: {result['title']}")
    
    # Show citations with 403 errors separately
    if citations_with_403:
        click.echo(f"\n{Fore.YELLOW}Citations where you should manually check the links due to a 403 error:{Style.RESET_ALL}")
        for result in citations_with_403:
            click.echo(f"- {result['id']}: {result['title']}")
    
    # Show citations where authors could not be verified
    if citations_no_metadata_verification:
        click.echo(f"\n{Fore.YELLOW}Citations where author list could not be verified:{Style.RESET_ALL}")
        click.echo(f"Count: {len(citations_no_metadata_verification)}\n")
        for result in citations_no_metadata_verification:
            click.echo(f"- {result['id']}: {result['title']}")

    # DOI verification via CrossRef
    doi_checked = [r for r in results if r['checks'].get('doi_correct') is not None]
    if doi_checked:
        doi_ok = [r for r in doi_checked if r['checks']['doi_correct'] is True]
        doi_bad = [r for r in doi_checked if r['checks']['doi_correct'] is False]
        click.echo(f"\n{Fore.CYAN}DOI verification via CrossRef:{Style.RESET_ALL}")
        click.echo(f"  Checked: {len(doi_checked)}, Verified: {len(doi_ok)}, Issues: {len(doi_bad)}")
        if doi_bad:
            click.echo(f"\n{Fore.YELLOW}Citations with DOI issues:{Style.RESET_ALL}")
            for result in doi_bad:
                click.echo(f"- {result['id']}: {result['title']}")


if __name__ == '__main__':
    main()